diff --git a/libraries/mongo-utils/batchedUpdate.js b/libraries/mongo-utils/batchedUpdate.js index 41af41f0d4..6e50d69e45 100644 --- a/libraries/mongo-utils/batchedUpdate.js +++ b/libraries/mongo-utils/batchedUpdate.js @@ -8,13 +8,20 @@ const READ_PREFERENCE_SECONDARY = : ReadPreference.secondaryPreferred.mode const ONE_MONTH_IN_MS = 1000 * 60 * 60 * 24 * 31 +/** @type {ObjectId | null} */ let ID_EDGE_PAST const ID_EDGE_FUTURE = objectIdFromMs(Date.now() + 1000) +/** @type {boolean} */ let BATCH_DESCENDING +/** @type {number} */ let BATCH_SIZE +/** @type {boolean} */ let VERBOSE_LOGGING +/** @type {ObjectId} */ let BATCH_RANGE_START +/** @type {ObjectId} */ let BATCH_RANGE_END +/** @type {number} */ let BATCH_MAX_TIME_SPAN_IN_MS let BATCHED_UPDATE_RUNNING = false @@ -55,7 +62,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) { if (BATCH_DESCENDING) { BATCH_RANGE_START = ID_EDGE_FUTURE } else { - BATCH_RANGE_START = ID_EDGE_PAST + BATCH_RANGE_START = /** @type {ObjectId} */ (ID_EDGE_PAST) } } BATCH_MAX_TIME_SPAN_IN_MS = parseInt( @@ -66,7 +73,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) { BATCH_RANGE_END = objectIdFromInput(options.BATCH_RANGE_END) } else { if (BATCH_DESCENDING) { - BATCH_RANGE_END = ID_EDGE_PAST + BATCH_RANGE_END = /** @type {ObjectId} */ (ID_EDGE_PAST) } else { BATCH_RANGE_END = ID_EDGE_FUTURE } diff --git a/libraries/mongo-utils/buildscript.txt b/libraries/mongo-utils/buildscript.txt index 6f30e9e1f6..f2be2fa89e 100644 --- a/libraries/mongo-utils/buildscript.txt +++ b/libraries/mongo-utils/buildscript.txt @@ -7,3 +7,4 @@ mongo-utils --node-version=24.13.0 --pipeline-owner=32 --public-repo=False +--tsconfig-no-implicit-any=True diff --git a/libraries/mongo-utils/test-utils.js b/libraries/mongo-utils/test-utils.js index 39ec1f0f90..0172c14bab 100644 --- a/libraries/mongo-utils/test-utils.js +++ b/libraries/mongo-utils/test-utils.js @@ -1,8 +1,8 @@ // @ts-check /** - * @import { MongoClient } from 'mongodb' - * @import { MongoClient as LegacyMongoClient } from 'mongodb-legacy' + * @typedef {import('mongodb').MongoClient} MongoClient + * @typedef {import('mongodb-legacy').MongoClient} LegacyMongoClient */ /** diff --git a/libraries/mongo-utils/tsconfig.json b/libraries/mongo-utils/tsconfig.json index 5c3833f9db..7dc569e499 100644 --- a/libraries/mongo-utils/tsconfig.json +++ b/libraries/mongo-utils/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.backend.json", + "compilerOptions": { + "noImplicitAny": true + }, "include": ["**/*.js", "**/*.cjs", "**/*.ts"] } diff --git a/libraries/object-persistor/buildscript.txt b/libraries/object-persistor/buildscript.txt index 91cd932de2..c60d511f4f 100644 --- a/libraries/object-persistor/buildscript.txt +++ b/libraries/object-persistor/buildscript.txt @@ -7,3 +7,4 @@ object-persistor --node-version=24.13.0 --pipeline-owner=32 --public-repo=False +--tsconfig-no-implicit-any=True diff --git a/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js b/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js index a14c5eda2d..06685b6d93 100644 --- a/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js +++ b/libraries/object-persistor/src/PerProjectEncryptedS3Persistor.js @@ -21,7 +21,7 @@ const hkdf = promisify(Crypto.hkdf) const AES256_KEY_LENGTH = 32 /** - * @typedef {Object} Settings + * @typedef {Object} EncryptionSettings * @property {boolean} automaticallyRotateDEKEncryption * @property {string} dataEncryptionKeyBucketName * @property {boolean} ignoreErrorsFromDEKReEncryption @@ -29,6 +29,26 @@ const AES256_KEY_LENGTH = 32 * @property {() => Promise>} getRootKeyEncryptionKeys */ +/** + * @typedef {Object} S3PersistorSettings + * @property {Object} [storageClass] + * @property {string} [key] + * @property {string} [secret] + * @property {string} [region] + * @property {string} [endpoint] + * @property {boolean} [pathStyle] + * @property {number} [maxRetries] + * @property {Object} [httpOptions] + * @property {string} [ca] + * @property {number} [signedUrlExpiryInMs] + * @property {number} [partSize] + * @property {Object} [bucketCreds] + */ + +/** + * @typedef {S3PersistorSettings & EncryptionSettings} Settings + */ + /** * @typedef {import('./types').ListDirectoryResult} ListDirectoryResult */ @@ -105,10 +125,6 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { }) } - async ensureKeyEncryptionKeysLoaded() { - await this.#availableKeyEncryptionKeysPromise - } - /** * @param {string} bucketName * @param {string} path @@ -289,6 +305,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { } } + /** + * @param {string} bucketName + * @param {string} path + * @param {NodeJS.ReadableStream} sourceStream + * @param {import('./S3Persistor.js').StreamOptions} opts + * @return {Promise} + */ async sendStream(bucketName, path, sourceStream, opts = {}) { const ssecOptions = opts.ssecOptions || @@ -299,6 +322,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { }) } + /** + * @param {string} bucketName + * @param {string} path + * @param {Object} opts + * @param {SSECOptions} [opts.ssecOptions] + * @return {Promise} + */ async getObjectStream(bucketName, path, opts = {}) { const ssecOptions = opts.ssecOptions || @@ -309,6 +339,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { }) } + /** + * @param {string} bucketName + * @param {string} path + * @param {Object} opts + * @param {SSECOptions} [opts.ssecOptions] + * @return {Promise} + */ async getObjectSize(bucketName, path, opts = {}) { const ssecOptions = opts.ssecOptions || @@ -316,6 +353,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { return await super.getObjectSize(bucketName, path, { ...opts, ssecOptions }) } + /** + * @param {string} bucketName + * @param {string} path + * @param {Object} opts + * @param {SSECOptions} [opts.ssecOptions] + * @return {Promise} + */ async getObjectStorageClass(bucketName, path, opts = {}) { const ssecOptions = opts.ssecOptions || @@ -326,11 +370,23 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { }) } + /** + * @param {string} bucketName + * @param {string} path + * @param {string} [continuationToken] + * @return {Promise} + */ async directorySize(bucketName, path, continuationToken) { // Note: Listing a bucket does not require SSE-C credentials. return await super.directorySize(bucketName, path, continuationToken) } + /** + * @param {string} bucketName + * @param {string} path + * @param {string} [continuationToken] + * @return {Promise} + */ async deleteDirectory(bucketName, path, continuationToken) { // Let [Settings.pathToProjectFolder] validate the project path before deleting things. const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path) @@ -344,12 +400,27 @@ class PerProjectEncryptedS3Persistor extends S3Persistor { } } + /** + * @param {string} bucketName + * @param {string} path + * @param {Object} opts + * @return {Promise} + */ async getObjectMd5Hash(bucketName, path, opts = {}) { // The ETag in object metadata is not the MD5 content hash, skip the HEAD request. opts = { ...opts, etagIsNotMD5: true } return await super.getObjectMd5Hash(bucketName, path, opts) } + /** + * @param {string} bucketName + * @param {string} sourcePath + * @param {string} destinationPath + * @param {Object} opts + * @param {SSECOptions} [opts.ssecOptions] + * @param {SSECOptions} [opts.ssecSrcOptions] + * @return {Promise} + */ async copyObject(bucketName, sourcePath, destinationPath, opts = {}) { const ssecOptions = opts.ssecOptions || diff --git a/libraries/object-persistor/src/S3Persistor.js b/libraries/object-persistor/src/S3Persistor.js index ac0075c718..4e6de1e9c0 100644 --- a/libraries/object-persistor/src/S3Persistor.js +++ b/libraries/object-persistor/src/S3Persistor.js @@ -77,14 +77,53 @@ class SSECOptions { } } +/** + * @typedef {import('@aws-sdk/client-s3').StorageClass} StorageClass + * @typedef {import('@aws-sdk/client-s3').PutObjectCommandInput} PutObjectCommandInput + * @typedef {import('@aws-sdk/client-s3').HeadObjectCommandOutput} HeadObjectCommandOutput + * @typedef {import('@aws-sdk/client-s3').GetObjectCommandOutput} GetObjectCommandOutput + * @typedef {import('@aws-sdk/client-s3').S3ClientConfig} S3ClientConfig + */ + +/** + * @typedef {Object} StreamOptions + * @property {string} [contentType] + * @property {string} [contentEncoding] + * @property {number} [contentLength] + * @property {'*'} [ifNoneMatch] + * @property {SSECOptions} [ssecOptions] + */ + +/** + * @typedef {Object} S3PersistorSettings + * @property {Object} [storageClass] + * @property {string} [key] + * @property {string} [secret] + * @property {string} [region] + * @property {string} [endpoint] + * @property {boolean} [pathStyle] + * @property {number} [maxRetries] + * @property {Object} [httpOptions] + * @property {string} [ca] + * @property {number} [signedUrlExpiryInMs] + * @property {number} [partSize] + * @property {Object} [bucketCreds] + */ + class S3Persistor extends AbstractPersistor { /** @type {Map} */ #clients = new Map() + /** @type {S3PersistorSettings} */ + settings + /** + * @param {S3PersistorSettings} [settings] + */ constructor(settings = {}) { super() - - settings.storageClass = settings.storageClass || {} + settings.storageClass = /** @type {Object} */ ( + settings.storageClass || {} + ) this.settings = settings } @@ -102,12 +141,7 @@ class S3Persistor extends AbstractPersistor { * @param {string} bucketName * @param {string} key * @param {NodeJS.ReadableStream} readStream - * @param {Object} opts - * @param {string} [opts.contentType] - * @param {string} [opts.contentEncoding] - * @param {number} [opts.contentLength] - * @param {'*'} [opts.ifNoneMatch] - * @param {SSECOptions} [opts.ssecOptions] + * @param {StreamOptions} opts * @return {Promise} */ async sendStream(bucketName, key, readStream, opts = {}) { @@ -128,8 +162,11 @@ class S3Persistor extends AbstractPersistor { Body: observer, } - if (this.settings.storageClass[bucketName]) { - uploadOptions.StorageClass = this.settings.storageClass[bucketName] + const storageClass = /** @type {Object} */ ( + this.settings.storageClass + ) + if (storageClass[bucketName]) { + uploadOptions.StorageClass = storageClass[bucketName] } if ('sourceMd5' in opts) { @@ -239,7 +276,9 @@ class S3Persistor extends AbstractPersistor { * @return {Promise} */ async getRedirectUrl(bucketName, key) { - const expiresSeconds = Math.round(this.settings.signedUrlExpiryInMs / 1000) + const expiresSeconds = Math.round( + /** @type {number} */ (this.settings.signedUrlExpiryInMs) / 1000 + ) try { return await getSignedUrl( this._getClientForBucket(bucketName), @@ -311,6 +350,7 @@ class S3Persistor extends AbstractPersistor { */ async #listDirectory(bucketName, key, continuationToken) { let response + /** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */ const options = { Bucket: bucketName, Prefix: key } if (continuationToken) { options.ContinuationToken = continuationToken @@ -501,6 +541,7 @@ class S3Persistor extends AbstractPersistor { */ async directorySize(bucketName, key, continuationToken) { try { + /** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */ const options = { Bucket: bucketName, Prefix: key, @@ -621,7 +662,7 @@ class S3Persistor extends AbstractPersistor { } /** - * @param {Object} bucketCredentials + * @param {{auth_key: string, auth_secret: string} | undefined} bucketCredentials * @param {import('@aws-sdk/client-s3').S3ClientConfig} clientOptions * @return {import('@aws-sdk/client-s3').S3ClientConfig} * @private @@ -637,7 +678,7 @@ class S3Persistor extends AbstractPersistor { } else if (this.settings.key) { options.credentials = { accessKeyId: this.settings.key, - secretAccessKey: this.settings.secret, + secretAccessKey: /** @type {string} */ (this.settings.secret), } } else { // Use the default credentials provider (process.env -> SSP -> ini -> IAM) @@ -709,6 +750,9 @@ class S3Persistor extends AbstractPersistor { } // test-only + /** + * @param {string} bucketName + */ _createBucket(bucketName) { return this._getClientForBucket(bucketName).send( new CreateBucketCommand({ Bucket: bucketName }) @@ -716,6 +760,10 @@ class S3Persistor extends AbstractPersistor { } // test-only + /** + * @param {string} bucketName + * @param {import('@aws-sdk/client-s3').PutObjectCommandInput} uploadOptions + */ _upload(bucketName, uploadOptions) { return this._getClientForBucket(bucketName).send( new PutObjectCommand(uploadOptions) diff --git a/libraries/object-persistor/tsconfig.json b/libraries/object-persistor/tsconfig.json index 5c3833f9db..7dc569e499 100644 --- a/libraries/object-persistor/tsconfig.json +++ b/libraries/object-persistor/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.backend.json", + "compilerOptions": { + "noImplicitAny": true + }, "include": ["**/*.js", "**/*.cjs", "**/*.ts"] } diff --git a/libraries/redis-wrapper/index.js b/libraries/redis-wrapper/index.js index 57ad9d9891..b019912913 100644 --- a/libraries/redis-wrapper/index.js +++ b/libraries/redis-wrapper/index.js @@ -4,6 +4,7 @@ const crypto = require('node:crypto') const os = require('node:os') const { promisify } = require('node:util') +// @ts-ignore - ioredis types may not be available in all contexts const Redis = require('ioredis') const { @@ -20,6 +21,9 @@ const PID = process.pid const RND = crypto.randomBytes(4).toString('hex') let COUNT = 0 +/** + * @param {any} opts + */ function createClient(opts) { const standardOpts = Object.assign({}, opts) delete standardOpts.key_schema @@ -42,6 +46,7 @@ function createClient(opts) { client = new Redis(standardOpts) } monkeyPatchIoRedisExec(client) + /** @param {any} callback */ client.healthCheck = callback => { if (callback) { // callback based invocation @@ -54,6 +59,9 @@ function createClient(opts) { return client } +/** + * @param {any} client + */ async function healthCheck(client) { // check the redis connection by storing and retrieving a unique key/value pair const uniqueToken = `host=${HOST}:pid=${PID}:random=${RND}:time=${Date.now()}:count=${COUNT++}` @@ -71,6 +79,11 @@ async function healthCheck(client) { }) } +/** + * @param {any} client + * @param {any} uniqueToken + * @param {any} context + */ async function runCheck(client, uniqueToken, context) { const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}` const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}` @@ -79,9 +92,11 @@ async function runCheck(client, uniqueToken, context) { context.stage = 'write' const writeAck = await client .set(healthCheckKey, healthCheckValue, 'EX', 60) - .catch(err => { - throw new RedisHealthCheckWriteError('write errored', context, err) - }) + .catch( + /** @param {any} err */ err => { + throw new RedisHealthCheckWriteError('write errored', context, err) + } + ) if (writeAck !== 'OK') { context.writeAck = writeAck throw new RedisHealthCheckWriteError('write failed', context) @@ -94,9 +109,15 @@ async function runCheck(client, uniqueToken, context) { .get(healthCheckKey) .del(healthCheckKey) .exec() - .catch(err => { - throw new RedisHealthCheckVerifyError('read/delete errored', context, err) - }) + .catch( + /** @param {any} err */ err => { + throw new RedisHealthCheckVerifyError( + 'read/delete errored', + context, + err + ) + } + ) if (roundTrippedHealthCheckValue !== healthCheckValue) { context.roundTrippedHealthCheckValue = roundTrippedHealthCheckValue throw new RedisHealthCheckVerifyError('read failed', context) @@ -107,6 +128,10 @@ async function runCheck(client, uniqueToken, context) { } } +/** + * @param {any} result + * @param {any} callback + */ function unwrapMultiResult(result, callback) { // ioredis exec returns a results like: // [ [null, 42], [null, "foo"] ] @@ -130,19 +155,30 @@ function unwrapMultiResult(result, callback) { } const unwrapMultiResultPromisified = promisify(unwrapMultiResult) +/** + * @param {any} client + */ function monkeyPatchIoRedisExec(client) { const _multi = client.multi client.multi = function () { const multi = _multi.apply(client, arguments) const _exec = multi.exec + /** @param {any} callback */ multi.exec = callback => { if (callback) { // callback based invocation - _exec.call(multi, (error, result) => { - // The command can fail all-together due to syntax errors - if (error) return callback(error) - unwrapMultiResult(result, callback) - }) + _exec.call( + multi, + /** + * @param {any} error + * @param {any} result + */ + (error, result) => { + // The command can fail all-together due to syntax errors + if (error) return callback(error) + unwrapMultiResult(result, callback) + } + ) } else { // Promise based invocation return _exec.call(multi).then(unwrapMultiResultPromisified) @@ -152,7 +188,11 @@ function monkeyPatchIoRedisExec(client) { } } +/** + * @param {{runner: any, timeout: any, context: any}} param0 + */ async function runWithTimeout({ runner, timeout, context }) { + /** @type {any} */ let healthCheckDeadline await Promise.race([ new Promise((resolve, reject) => { diff --git a/libraries/settings/Settings.js b/libraries/settings/Settings.js index 3a5f31c33c..d028e633e8 100644 --- a/libraries/settings/Settings.js +++ b/libraries/settings/Settings.js @@ -45,7 +45,7 @@ if (!settingsExist) { console.warn("No settings or defaults found. I'm flying blind.") } -module.exports = settings +module.exports = /** @type {any} */ (settings) function pathIfExists(path) { if (path && fs.existsSync(path)) { diff --git a/package-lock.json b/package-lock.json index b1f0e09ee3..226a476472 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16409,6 +16409,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/async": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.25.tgz", + "integrity": "sha512-O6Th/DI18XjrL9TX8LO9F/g26qAz5vynmQqlXt/qLGrskvzCKXKc5/tATz3G2N6lM8eOf3M8/StB14FncAmocg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aws-lambda": { "version": "8.10.160", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz", @@ -17176,6 +17183,95 @@ "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, + "node_modules/@types/sanitize-html": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz", + "integrity": "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, + "node_modules/@types/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -17305,6 +17401,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/utf-8-validate": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/utf-8-validate/-/utf-8-validate-5.0.2.tgz", + "integrity": "sha512-ta7cOkEiNr0RGKARljNBaI7E1GBIr3VwS9RrSoQRmbdv1RVq7Q6VhjSGmQHYNt3nHn051qZBKKrpnw7cnEMDuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -53195,6 +53301,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/algoliasearch": "^3.34.11", + "@types/async": "^3.2.25", "@types/bootstrap": "^5.2.10", "@types/chai": "^4.3.0", "@types/dateformat": "^5.0.3", @@ -53202,6 +53309,7 @@ "@types/dom-speech-recognition": "^0.0.7", "@types/events": "^3.0.3", "@types/express": "^4.17.23", + "@types/minimist": "^1.2.5", "@types/mocha": "^9.1.0", "@types/mocha-each": "^2.0.4", "@types/node": "^24.5.2", @@ -53212,7 +53320,9 @@ "@types/react-linkify": "^1.0.4", "@types/react-syntax-highlighter": "^15.5.11", "@types/recurly__recurly-js": "^4.38.0", + "@types/sanitize-html": "^2.14.0", "@types/sinon-chai": "^3.2.12", + "@types/utf-8-validate": "^5.0.2", "@types/uuid": "^9.0.8", "@uppy/core": "^3.8.0", "@uppy/dashboard": "^3.7.1", diff --git a/services/web/app/src/Features/Analytics/EmailChangeHelper.mjs b/services/web/app/src/Features/Analytics/EmailChangeHelper.mjs index 3b1bc5b665..ebfe62f94f 100644 --- a/services/web/app/src/Features/Analytics/EmailChangeHelper.mjs +++ b/services/web/app/src/Features/Analytics/EmailChangeHelper.mjs @@ -50,7 +50,9 @@ async function registerEmailUpdate(userId, email, eventData = {}) { */ async function makeEmailChangeEvent(userId, email, eventData) { const userEmails = await UserGetter.promises.getUserFullEmails(userId) - const emailData = userEmails.find(userEmail => userEmail.email === email) + const emailData = userEmails.find( + /** @param {any} userEmail */ userEmail => userEmail.email === email + ) const filledEventData = fillMissingEventData(eventData, emailData) diff --git a/services/web/app/src/Features/Authorization/PermissionsController.mjs b/services/web/app/src/Features/Authorization/PermissionsController.mjs index 29b3ef4ae2..bbfbdf9754 100644 --- a/services/web/app/src/Features/Authorization/PermissionsController.mjs +++ b/services/web/app/src/Features/Authorization/PermissionsController.mjs @@ -25,10 +25,16 @@ const { * @returns {() => (req: Request, res: Response, next: NextFunction) => void} The middleware function that adds the `assertPermission` function to the request object. */ function useCapabilities() { + /** + * @param {any} req + * @param {any} res + * @param {any} next + */ const middleware = async function (req, res, next) { // attach the user's capabilities to the request object req.capabilitySet = new Set() // provide a function to assert that a capability is present + /** @param {any} capability */ req.assertPermission = capability => { if (!req.capabilitySet.has(capability)) { throw new ForbiddenError( @@ -52,7 +58,9 @@ function useCapabilities() { if (results.length > 0) { // get the combined group policy applying to the user - const groupPolicies = results.map(result => result.groupPolicy) + const groupPolicies = results.map( + /** @param {any} result */ result => result.groupPolicy + ) const combinedGroupPolicy = combineGroupPolicies(groupPolicies) // attach the new capabilities to the request object for (const cap of getUserCapabilities(combinedGroupPolicy)) { @@ -68,7 +76,7 @@ function useCapabilities() { } } next() - } catch (error) { + } catch (/** @type {any} */ error) { if (error instanceof UserNotFoundError) { // the user is logged in but doesn't exist in the database // this can happen if the user has just deleted their account @@ -89,7 +97,10 @@ function useCapabilities() { function requirePermission(...requiredCapabilities) { if ( requiredCapabilities.length === 0 || - requiredCapabilities.some(capability => typeof capability !== 'string') + requiredCapabilities.some( + /** @param {any} capability */ capability => + typeof capability !== 'string' + ) ) { throw new Error('invalid required capabilities') } @@ -102,16 +113,14 @@ function requirePermission(...requiredCapabilities) { if (!Features.hasFeature('saas')) { return next() } - if (!req.user && !req.oauth_user) { + const user = req.user || req.oauth_user + if (!user) { return next(new Error('no user')) } try { - await assertUserPermissions( - req.user || req.oauth_user, - requiredCapabilities - ) + await assertUserPermissions(user, requiredCapabilities) next() - } catch (error) { + } catch (/** @type {any} */ error) { next(error) } } diff --git a/services/web/app/src/Features/Chat/ChatApiHandler.mjs b/services/web/app/src/Features/Chat/ChatApiHandler.mjs index e14369ad12..facf8a44cf 100644 --- a/services/web/app/src/Features/Chat/ChatApiHandler.mjs +++ b/services/web/app/src/Features/Chat/ChatApiHandler.mjs @@ -5,24 +5,44 @@ import { fetchJson, fetchNothing } from '@overleaf/fetch-utils' import settings from '@overleaf/settings' import { callbackify } from 'node:util' +/** + * @param {any} projectId + * @param {any} threadId + */ async function getThread(projectId, threadId) { return await fetchJson(chatApiUrl(`/project/${projectId}/thread/${threadId}`)) } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} messageId + */ async function getThreadMessage(projectId, threadId, messageId) { return await fetchJson( chatApiUrl(`/project/${projectId}/thread/${threadId}/messages/${messageId}`) ) } +/** + * @param {any} projectId + */ async function getThreads(projectId) { return await fetchJson(chatApiUrl(`/project/${projectId}/threads`)) } +/** + * @param {any} projectId + */ async function destroyProject(projectId) { await fetchNothing(chatApiUrl(`/project/${projectId}`), { method: 'DELETE' }) } +/** + * @param {any} projectId + * @param {any} userId + * @param {any} content + */ async function sendGlobalMessage(projectId, userId, content) { const message = await fetchJson( chatApiUrl(`/project/${projectId}/messages`), @@ -34,6 +54,11 @@ async function sendGlobalMessage(projectId, userId, content) { return message } +/** + * @param {any} projectId + * @param {any} limit + * @param {any} before + */ async function getGlobalMessages(projectId, limit, before) { const url = chatApiUrl(`/project/${projectId}/messages`) if (limit != null) { @@ -46,12 +71,22 @@ async function getGlobalMessages(projectId, limit, before) { return await fetchJson(url) } +/** + * @param {any} projectId + * @param {any} messageId + */ async function getGlobalMessage(projectId, messageId) { return await fetchJson( chatApiUrl(`/project/${projectId}/messages/${messageId}`) ) } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} userId + * @param {any} content + */ async function sendComment(projectId, threadId, userId, content) { const comment = await fetchJson( chatApiUrl(`/project/${projectId}/thread/${threadId}/messages`), @@ -63,6 +98,11 @@ async function sendComment(projectId, threadId, userId, content) { return comment } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} userId + */ async function resolveThread(projectId, threadId, userId) { await fetchNothing( chatApiUrl(`/project/${projectId}/thread/${threadId}/resolve`), @@ -73,6 +113,10 @@ async function resolveThread(projectId, threadId, userId) { ) } +/** + * @param {any} projectId + * @param {any} threadId + */ async function reopenThread(projectId, threadId) { await fetchNothing( chatApiUrl(`/project/${projectId}/thread/${threadId}/reopen`), @@ -80,12 +124,23 @@ async function reopenThread(projectId, threadId) { ) } +/** + * @param {any} projectId + * @param {any} threadId + */ async function deleteThread(projectId, threadId) { await fetchNothing(chatApiUrl(`/project/${projectId}/thread/${threadId}`), { method: 'DELETE', }) } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} messageId + * @param {any} userId + * @param {any} content + */ async function editMessage(projectId, threadId, messageId, userId, content) { await fetchNothing( chatApiUrl( @@ -98,6 +153,12 @@ async function editMessage(projectId, threadId, messageId, userId, content) { ) } +/** + * @param {any} projectId + * @param {any} messageId + * @param {any} userId + * @param {any} content + */ async function editGlobalMessage(projectId, messageId, userId, content) { await fetchNothing( chatApiUrl(`/project/${projectId}/messages/${messageId}/edit`), @@ -108,6 +169,11 @@ async function editGlobalMessage(projectId, messageId, userId, content) { ) } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} messageId + */ async function deleteMessage(projectId, threadId, messageId) { await fetchNothing( chatApiUrl( @@ -117,6 +183,12 @@ async function deleteMessage(projectId, threadId, messageId) { ) } +/** + * @param {any} projectId + * @param {any} threadId + * @param {any} userId + * @param {any} messageId + */ async function deleteUserMessage(projectId, threadId, userId, messageId) { await fetchNothing( chatApiUrl( @@ -126,6 +198,10 @@ async function deleteUserMessage(projectId, threadId, userId, messageId) { ) } +/** + * @param {any} projectId + * @param {any} messageId + */ async function deleteGlobalMessage(projectId, messageId) { await fetchNothing( chatApiUrl(`/project/${projectId}/messages/${messageId}`), @@ -133,6 +209,9 @@ async function deleteGlobalMessage(projectId, messageId) { ) } +/** + * @param {any} projectId + */ async function getResolvedThreadIds(projectId) { const body = await fetchJson( chatApiUrl(`/project/${projectId}/resolved-thread-ids`) @@ -140,6 +219,10 @@ async function getResolvedThreadIds(projectId) { return body.resolvedThreadIds } +/** + * @param {any} projectId + * @param {any} threads + */ async function duplicateCommentThreads(projectId, threads) { return await fetchJson( chatApiUrl(`/project/${projectId}/duplicate-comment-threads`), @@ -152,6 +235,10 @@ async function duplicateCommentThreads(projectId, threads) { ) } +/** + * @param {any} projectId + * @param {any} threads + */ async function generateThreadData(projectId, threads) { return await fetchJson( chatApiUrl(`/project/${projectId}/generate-thread-data`), @@ -162,6 +249,9 @@ async function generateThreadData(projectId, threads) { ) } +/** + * @param {any} path + */ function chatApiUrl(path) { return new URL(path, settings.apis.chat.internal_url) } diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.mjs b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.mjs index ec77e51c22..320429da5e 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.mjs +++ b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.mjs @@ -212,6 +212,9 @@ class ProjectAccess { } } +/** + * @param {any} projectId + */ async function getProjectAccess(projectId) { const project = await ProjectGetter.promises.getProject(projectId, { owner_ref: 1, @@ -230,18 +233,33 @@ async function getProjectAccess(projectId) { return new ProjectAccess(project) } +/** + * @param {any} projectId + */ async function getMemberIdsWithPrivilegeLevels(projectId) { return (await getProjectAccess(projectId)).allMembers() } +/** + * @param {any} projectId + */ async function getMemberIds(projectId) { return (await getProjectAccess(projectId)).memberIds() } +/** + * @param {any} projectId + */ async function getInvitedMemberIds(projectId) { return (await getProjectAccess(projectId)).invitedMemberIds() } +/** + * @param {any} ownerId + * @param {any} collaboratorIds + * @param {any} readOnlyIds + * @param {any} reviewerIds + */ async function getInvitedMembersWithPrivilegeLevelsFromFields( ownerId, collaboratorIds, @@ -262,6 +280,10 @@ async function getInvitedMembersWithPrivilegeLevelsFromFields( return _loadMembers(members) } +/** + * @param {any} userId + * @param {any} projectId + */ async function getMemberIdPrivilegeLevel(userId, projectId) { // In future if the schema changes and getting all member ids is more expensive (multiple documents) // then optimise this. @@ -271,14 +293,24 @@ async function getMemberIdPrivilegeLevel(userId, projectId) { return (await getProjectAccess(projectId)).privilegeLevelForUser(userId) } +/** + * @param {any} projectId + */ async function getInvitedEditCollaboratorCount(projectId) { return (await getProjectAccess(projectId)).countInvitedEditCollaborators() } +/** + * @param {any} projectId + */ async function getInvitedPendingEditorCount(projectId) { return (await getProjectAccess(projectId)).countInvitedPendingEditors() } +/** + * @param {any} userId + * @param {any} projectId + */ async function isUserInvitedMemberOfProject(userId, projectId) { if (!userId) { return false @@ -286,6 +318,10 @@ async function isUserInvitedMemberOfProject(userId, projectId) { return (await getProjectAccess(projectId)).isUserInvitedMember(userId) } +/** + * @param {any} userId + * @param {any} projectId + */ async function isUserInvitedReadWriteMemberOfProject(userId, projectId) { if (!userId) { return false @@ -295,6 +331,10 @@ async function isUserInvitedReadWriteMemberOfProject(userId, projectId) { ) } +/** + * @param {any} userId + * @param {any} projectId + */ async function getPublicShareTokens(userId, projectId) { const memberInfo = await Project.findOne( { @@ -335,6 +375,10 @@ async function getPublicShareTokens(userId, projectId) { // This function returns all the projects that a user currently has access to, // excluding projects where the user is listed in the token access fields when // token access has been disabled. +/** + * @param {any} userId + * @param {any} fields + */ async function getProjectsUserIsMemberOf(userId, fields) { // @ts-ignore const limit = pLimit(2) @@ -368,6 +412,10 @@ async function getProjectsUserIsMemberOf(userId, fields) { // This function returns all the projects that a user is a member of, regardless of // the current state of the project, so it includes those projects where token access // has been disabled. +/** + * @param {any} userId + * @param {any} fields + */ async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) { const readAndWrite = await Project.find( { collaberator_refs: userId }, @@ -385,6 +433,9 @@ async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) { return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } } +/** + * @param {any} projectId + */ async function getAllInvitedMembers(projectId) { try { const projectAccess = await getProjectAccess(projectId) @@ -395,6 +446,10 @@ async function getAllInvitedMembers(projectId) { } } +/** + * @param {any} userId + * @param {any} projectId + */ async function userIsTokenMember(userId, projectId) { userId = new ObjectId(userId.toString()) projectId = new ObjectId(projectId.toString()) @@ -413,6 +468,10 @@ async function userIsTokenMember(userId, projectId) { return project != null } +/** + * @param {any} userId + * @param {any} projectId + */ async function userIsReadWriteTokenMember(userId, projectId) { userId = new ObjectId(userId.toString()) projectId = new ObjectId(projectId.toString()) @@ -476,15 +535,20 @@ function _getMemberIdsWithPrivilegeLevelsFromFields( } for (const memberId of readOnlyIds || []) { + /** @type {ProjectMember} */ const record = { id: memberId.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY, source: Sources.INVITE, } - if (pendingEditorIds?.some(pe => memberId.equals(pe))) { + if ( + pendingEditorIds?.some(/** @param {any} pe */ pe => memberId.equals(pe)) + ) { record.pendingEditor = true - } else if (pendingReviewerIds?.some(pr => memberId.equals(pr))) { + } else if ( + pendingReviewerIds?.some(/** @param {any} pr */ pr => memberId.equals(pr)) + ) { record.pendingReviewer = true } members.push(record) @@ -533,6 +597,7 @@ async function _loadMembers(members) { .map(member => { const user = users.get(member.id) if (!user) return null + /** @type {any} */ const record = { user, privilegeLevel: member.privilegeLevel, diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.mjs b/services/web/app/src/Features/Docstore/DocstoreManager.mjs index ecba8df961..2b6bbc67a9 100644 --- a/services/web/app/src/Features/Docstore/DocstoreManager.mjs +++ b/services/web/app/src/Features/Docstore/DocstoreManager.mjs @@ -238,7 +238,13 @@ async function isDocDeleted(projectId, docId) { * @param ranges * @return {Promise<{modified: *, rev: *}>} */ -async function updateDoc(projectId, docId, lines, version, ranges) { +async function updateDoc( + projectId, + docId, + lines, + version, + /** @type {any} */ ranges +) { const url = new URL(settings.apis.docstore.url) url.pathname = path.posix.join('project', projectId, 'doc', docId) try { diff --git a/services/web/app/src/Features/FileStore/FileStoreController.mjs b/services/web/app/src/Features/FileStore/FileStoreController.mjs index 3433fa5dea..ab5e4f014b 100644 --- a/services/web/app/src/Features/FileStore/FileStoreController.mjs +++ b/services/web/app/src/Features/FileStore/FileStoreController.mjs @@ -9,6 +9,10 @@ import HistoryManager from '../History/HistoryManager.mjs' import Errors from '../Errors/Errors.js' import { preparePlainTextResponse } from '../../infrastructure/Response.mjs' +/** + * @param {any} req + * @param {any} res + */ async function getFile(req, res) { const projectId = req.params.Project_id const fileId = req.params.File_id @@ -101,6 +105,10 @@ async function getFile(req, res) { } } +/** + * @param {any} req + * @param {any} res + */ async function getFileHead(req, res) { const projectId = req.params.Project_id const fileId = req.params.File_id @@ -159,6 +167,9 @@ async function getFileHead(req, res) { res.status(200).end() } +/** + * @param {any} file + */ function isHtml(file) { return ( fileEndsWith(file, '.html') || @@ -167,6 +178,10 @@ function isHtml(file) { ) } +/** + * @param {any} file + * @param {any} ext + */ function fileEndsWith(file, ext) { return ( file.name != null && @@ -175,6 +190,9 @@ function fileEndsWith(file, ext) { ) } +/** + * @param {any} userAgent + */ function isMobileSafari(userAgent) { return ( userAgent && diff --git a/services/web/app/src/Features/History/HistoryController.mjs b/services/web/app/src/Features/History/HistoryController.mjs index 0887098dd6..be9e01c006 100644 --- a/services/web/app/src/Features/History/HistoryController.mjs +++ b/services/web/app/src/Features/History/HistoryController.mjs @@ -15,17 +15,24 @@ import { } from '@overleaf/fetch-utils' import settings from '@overleaf/settings' +/** @type {any} */ import SessionManager from '../Authentication/SessionManager.mjs' import UserGetter from '../User/UserGetter.mjs' +/** @type {any} */ import ProjectGetter from '../Project/ProjectGetter.mjs' import Errors from '../Errors/Errors.js' +/** @type {any} */ import HistoryManager from './HistoryManager.mjs' +/** @type {any} */ import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs' +/** @type {any} */ import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.mjs' +/** @type {any} */ import RestoreManager from './RestoreManager.mjs' import { prepareZipAttachment } from '../../infrastructure/Response.mjs' import Features from '../../infrastructure/Features.mjs' import { z, zz, parseReq } from '../../infrastructure/Validation.mjs' +/** @type {any} */ import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs' // Number of seconds after which the browser should send a request to revalidate @@ -38,10 +45,18 @@ const STALE_WHILE_REVALIDATE_SECONDS = 365 * 86400 // 1 year const MAX_HISTORY_ZIP_ATTEMPTS = 40 +/** + * @param {any} req + * @param {any} res + */ async function getBlob(req, res) { await requestBlob('GET', req, res) } +/** + * @param {any} req + * @param {any} res + */ async function headBlob(req, res) { await requestBlob('HEAD', req, res) } @@ -56,6 +71,11 @@ const requestBlobSchema = z.object({ }), }) +/** + * @param {any} method + * @param {any} req + * @param {any} res + */ async function requestBlob(method, req, res) { const { params } = parseReq(req, requestBlobSchema) const { project_id: projectId, hash } = params @@ -76,7 +96,7 @@ async function requestBlob(method, req, res) { method, range )) - } catch (err) { + } catch (/** @type {any} */ err) { if (err instanceof Errors.NotFoundError) return res.status(404).end() throw err } @@ -94,7 +114,7 @@ async function requestBlob(method, req, res) { try { await pipeline(stream, res) - } catch (err) { + } catch (/** @type {any} */ err) { // If the downstream request is cancelled, we get an // ERR_STREAM_PREMATURE_CLOSE, ignore these "errors". if (!isPrematureClose(err)) { @@ -103,6 +123,10 @@ async function requestBlob(method, req, res) { } } +/** + * @param {any} res + * @param {any} etag + */ function setBlobCacheHeaders(res, etag) { // Blobs are immutable, so they can in principle be cached indefinitely. Here, // we ask the browser to cache them for some time, but then check back @@ -115,6 +139,11 @@ function setBlobCacheHeaders(res, etag) { res.set('ETag', etag) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function proxyToHistoryApi(req, res, next) { const userId = SessionManager.getLoggedInUserId(req.session) const url = settings.apis.project_history.url + req.url @@ -135,7 +164,7 @@ async function proxyToHistoryApi(req, res, next) { try { await pipeline(stream, res) - } catch (err) { + } catch (/** @type {any} */ err) { // If the downstream request is cancelled, we get an // ERR_STREAM_PREMATURE_CLOSE. if (!isPrematureClose(err)) { @@ -144,6 +173,11 @@ async function proxyToHistoryApi(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function proxyToHistoryApiAndInjectUserDetails(req, res, next) { const userId = SessionManager.getLoggedInUserId(req.session) const url = settings.apis.project_history.url + req.url @@ -155,6 +189,11 @@ async function proxyToHistoryApiAndInjectUserDetails(req, res, next) { res.json(data) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function resyncProjectHistory(req, res, next) { // increase timeout to 6 minutes res.setTimeout(6 * 60 * 1000) @@ -173,7 +212,7 @@ async function resyncProjectHistory(req, res, next) { projectId, opts ) - } catch (err) { + } catch (/** @type {any} */ err) { if (err instanceof Errors.ProjectHistoryDisabledError) { return res.sendStatus(404) } else { @@ -184,6 +223,11 @@ async function resyncProjectHistory(req, res, next) { res.sendStatus(204) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function restoreFileFromV2(req, res, next) { const { project_id: projectId } = req.params const { version, pathname } = req.body @@ -215,6 +259,11 @@ async function restoreFileFromV2(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function revertFile(req, res, next) { const { project_id: projectId } = req.params const { version, pathname } = req.body @@ -247,6 +296,11 @@ async function revertFile(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function revertProject(req, res, next) { const { project_id: projectId } = req.params const { version } = req.body @@ -273,6 +327,11 @@ async function revertProject(req, res, next) { res.json(reverted) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function getLabels(req, res, next) { const projectId = req.params.Project_id @@ -284,6 +343,11 @@ async function getLabels(req, res, next) { res.json(labels) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function createLabel(req, res, next) { const projectId = req.params.Project_id const { comment, version } = req.body @@ -301,6 +365,9 @@ async function createLabel(req, res, next) { res.json(label) } +/** + * @param {any} label + */ async function _enrichLabel(label) { const newLabel = Object.assign({}, label) if (!label.user_id) { @@ -317,11 +384,16 @@ async function _enrichLabel(label) { return newLabel } +/** + * @param {any} labels + */ async function _enrichLabels(labels) { if (!labels || !labels.length) { return [] } - const uniqueUsers = new Set(labels.map(label => label.user_id)) + const uniqueUsers = new Set( + labels.map(/** @param {any} label */ label => label.user_id) + ) // For backwards compatibility, and for anonymously created labels in SP // expect missing user_id fields @@ -336,15 +408,22 @@ async function _enrichLabels(labels) { last_name: 1, email: 1, }) - const users = new Map(rawUsers.map(user => [String(user._id), user])) + const users = new Map( + rawUsers.map(/** @param {any} user */ user => [String(user._id), user]) + ) - labels.forEach(label => { - const user = users.get(label.user_id) - label.user_display_name = _displayNameForUser(user) - }) + labels.forEach( + /** @param {any} label */ label => { + const user = users.get(label.user_id) + label.user_display_name = _displayNameForUser(user) + } + ) return labels } +/** + * @param {any} user + */ function _displayNameForUser(user) { if (user == null) { return 'Anonymous' @@ -365,6 +444,11 @@ function _displayNameForUser(user) { return name } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function deleteLabel(req, res, next) { const { Project_id: projectId, label_id: labelId } = req.params const userId = SessionManager.getLoggedInUserId(req.session) @@ -393,11 +477,17 @@ const downloadZipOfVersionSchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function downloadZipOfVersion(req, res, next) { const { params } = parseReq(req, downloadZipOfVersionSchema) const { project_id: projectId, version } = params const userId = SessionManager.getLoggedInUserId(req.session) + /** @type {any} */ const project = await ProjectDetailsHandler.promises.getDetails(projectId) const v1Id = project.overleaf && project.overleaf.history && project.overleaf.history.id @@ -430,6 +520,13 @@ async function downloadZipOfVersion(req, res, next) { ) } +/** + * @param {any} v1ProjectId + * @param {any} version + * @param {any} name + * @param {any} req + * @param {any} res + */ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { if (req.destroyed) { // client has disconnected -- skip project history api call and download @@ -447,7 +544,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { let stream try { stream = await fetchStream(url, { basicAuth }) - } catch (err) { + } catch (/** @type {any} */ err) { if (err instanceof RequestFailedError && err.response.status === 404) { return res.sendStatus(404) } else { @@ -459,7 +556,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { try { await pipeline(stream, res) - } catch (err) { + } catch (/** @type {any} */ err) { // If the downstream request is cancelled, we get an // ERR_STREAM_PREMATURE_CLOSE. if (!isPrematureClose(err)) { @@ -472,7 +569,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { let body try { body = await fetchJson(url, { method: 'POST', basicAuth }) - } catch (err) { + } catch (/** @type {any} */ err) { if (err instanceof RequestFailedError && err.response.status === 404) { throw new Errors.NotFoundError('zip not found') } else { @@ -513,7 +610,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) { const stream = await fetchStream(body.zipUrl) prepareZipAttachment(res, `${name}.zip`) await pipeline(stream, res) - } catch (err) { + } catch (/** @type {any} */ err) { if (attempt > MAX_HISTORY_ZIP_ATTEMPTS) { throw err } @@ -545,6 +642,11 @@ const getLatestHistorySchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function getLatestHistory(req, res, next) { const { params } = parseReq(req, getLatestHistorySchema) const projectId = params.project_id @@ -562,6 +664,11 @@ const getChangesSchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function getChanges(req, res, next) { const { params, query } = parseReq(req, getChangesSchema) const projectId = params.project_id @@ -598,6 +705,9 @@ async function getChanges(req, res, next) { } } +/** + * @param {any} err + */ function isPrematureClose(err) { return ( err instanceof Error && diff --git a/services/web/app/src/Features/History/HistoryRangesSupportMigration.mjs b/services/web/app/src/Features/History/HistoryRangesSupportMigration.mjs index b57ff81fc3..51e96baf9b 100644 --- a/services/web/app/src/Features/History/HistoryRangesSupportMigration.mjs +++ b/services/web/app/src/Features/History/HistoryRangesSupportMigration.mjs @@ -71,6 +71,9 @@ async function migrateProjects(opts = {}) { .sort({ _id: -1 }) let terminating = false + /** + * @param {any} signal + */ const handleSignal = signal => { logger.info({ signal }, 'History ranges support migration received signal') terminating = true @@ -78,6 +81,7 @@ async function migrateProjects(opts = {}) { process.on('SIGINT', handleSignal) process.on('SIGTERM', handleSignal) + /** @type {{ quick: number; skipped: number; resync: number; total: number; [key: string]: number }} */ const projectsProcessed = { quick: 0, skipped: 0, @@ -120,35 +124,39 @@ async function migrateProjects(opts = {}) { } const job = processProject(projectId, direction, quickOnly) - .then(info => { - jobsByProjectId.delete(projectId) - projectsProcessed[info.migrationType] += 1 - projectsProcessed.total += 1 - logger.debug( - { - projectId, - direction, - projectsProcessed, - errors, - ...info, - }, - 'History ranges support migration' - ) - if (projectsProcessed.total % 10000 === 0) { - logger.info( - { projectsProcessed, errors, lastProjectId: projectId }, - 'History ranges support migration progress' + .then( + /** @param {any} info */ info => { + jobsByProjectId.delete(projectId) + projectsProcessed[info.migrationType] += 1 + projectsProcessed.total += 1 + logger.debug( + { + projectId, + direction, + projectsProcessed, + errors, + ...info, + }, + 'History ranges support migration' + ) + if (projectsProcessed.total % 10000 === 0) { + logger.info( + { projectsProcessed, errors, lastProjectId: projectId }, + 'History ranges support migration progress' + ) + } + } + ) + .catch( + /** @param {any} err */ err => { + jobsByProjectId.delete(projectId) + errors += 1 + logger.error( + { err, projectId, direction, projectsProcessed, errors }, + 'Failed to migrate history ranges support' ) } - }) - .catch(err => { - jobsByProjectId.delete(projectId) - errors += 1 - logger.error( - { err, projectId, direction, projectsProcessed, errors }, - 'Failed to migrate history ranges support' - ) - }) + ) jobsByProjectId.set(projectId, job) } @@ -198,7 +206,7 @@ async function quickMigration(projectId, direction = 'forwards') { try { projectHasRanges = await DocstoreManager.promises.projectHasRanges(projectId) - } catch (err) { + } catch (/** @type {any} */ err) { // Docstore request probably timed out. Assume the project has ranges logger.warn( { err, projectId }, @@ -216,7 +224,7 @@ async function quickMigration(projectId, direction = 'forwards') { projectId, direction === 'forwards' ) - } catch (err) { + } catch (/** @type {any} */ err) { await DocumentUpdaterHandler.promises.unblockProject(projectId) await hardResyncProject(projectId) throw err @@ -225,7 +233,7 @@ async function quickMigration(projectId, direction = 'forwards') { let wasBlocked try { wasBlocked = await DocumentUpdaterHandler.promises.unblockProject(projectId) - } catch (err) { + } catch (/** @type {any} */ err) { await hardResyncProject(projectId) throw err } diff --git a/services/web/app/src/Features/History/HistoryRouter.mjs b/services/web/app/src/Features/History/HistoryRouter.mjs index abf43eb486..5db1d689b4 100644 --- a/services/web/app/src/Features/History/HistoryRouter.mjs +++ b/services/web/app/src/Features/History/HistoryRouter.mjs @@ -23,6 +23,10 @@ const rateLimiters = { }), } +/** + * @param {any} webRouter + * @param {any} privateApiRouter + */ function apply(webRouter, privateApiRouter) { // Blobs diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.mjs b/services/web/app/src/Features/Notifications/NotificationsBuilder.mjs index 84d37c802f..cbfac1c907 100644 --- a/services/web/app/src/Features/Notifications/NotificationsBuilder.mjs +++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.mjs @@ -260,6 +260,7 @@ function oldDebugProjects(userId) { } } +/** @type {Record} */ const NotificationsBuilder = { // Note: notification keys should be url-safe dropboxUnlinkedDueToLapsedReconfirmation(userId) { @@ -291,6 +292,7 @@ const NotificationsBuilder = { }, } +/** @type {Record} */ NotificationsBuilder.promises = { dropboxUnlinkedDueToLapsedReconfirmation, redundantPersonalSubscription, diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index ef6c668244..0080f2fd69 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -42,8 +42,8 @@ import UserSettingsHelper from './UserSettingsHelper.mjs' /** * @param {Affiliation} affiliation - * @param session - * @param linkedInstitutionIds + * @param {any} session + * @param {string[]} linkedInstitutionIds * @returns {boolean} * @private */ @@ -99,6 +99,9 @@ const _buildPortalTemplatesList = affiliations => { return portalTemplates } +/** + * @param {any} req + */ function cleanupSession(req) { // cleanup redirects at the end of the redirect chain delete req.session.postCheckoutRedirect @@ -124,10 +127,12 @@ async function projectListPage(req, res, next) { // - object - the subscription data object let usersBestSubscription let usersIndividualSubscription + /** @type {any[]} */ let usersGroupSubscriptions = [] let usersManagedGroupSubscriptions = [] let survey let userIsMemberOfGroupSubscription = false + /** @type {any[]} */ let groupSubscriptionsPendingEnrollment = [] const isSaas = Features.hasFeature('saas') @@ -153,7 +158,8 @@ async function projectListPage(req, res, next) { if (groupsWithEmails && groupsWithEmails.length > 0) { if ( groupsWithEmails.some( - ({ subscription }) => subscription.managedUsersEnabled + (/** @type {any} */ { subscription }) => + subscription.managedUsersEnabled ) ) { return res.redirect('/domain-capture') @@ -338,6 +344,7 @@ async function projectListPage(req, res, next) { reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed']) const samlSession = req.session.saml // Notification: SSO Available + /** @type {string[]} */ const linkedInstitutionIds = [] userEmails.forEach(email => { if (email.samlProviderId) { @@ -906,6 +913,9 @@ function _hasActiveFilter(filters) { ) } +/** + * @param {any} user + */ // todo: quota clean-up: rename function and vars async function _userHasAIAssist(user) { let hasPremiumAiFeatures @@ -937,6 +947,9 @@ async function _userHasAIAssist(user) { // Determines if user is able to enable AI assist // based on their permissions and settings // It does NOT determine if the user has AI Assist enabled +/** + * @param {any} user + */ async function _canUseAIAssist(user) { // Check if the assistant has been manually disabled by the user // post https://github.com/overleaf/internal/pull/31273 we can rely on user.aiFeatures being populated diff --git a/services/web/app/src/Features/Spelling/SpellingController.mjs b/services/web/app/src/Features/Spelling/SpellingController.mjs index 1ad42655f3..fecb3193e6 100644 --- a/services/web/app/src/Features/Spelling/SpellingController.mjs +++ b/services/web/app/src/Features/Spelling/SpellingController.mjs @@ -17,23 +17,41 @@ const unlearnSchema = z.object({ }) export default { + /** + * @param {any} req + * @param {any} res + * @param {any} next + */ learn(req, res, next) { const { body } = parseReq(req, learnSchema) const { word } = body const userId = SessionManager.getLoggedInUserId(req.session) - LearnedWordsManager.learnWord(userId, word, err => { - if (err) return next(err) - res.sendStatus(204) - }) + LearnedWordsManager.learnWord( + userId, + word, + /** @param {any} err */ err => { + if (err) return next(err) + res.sendStatus(204) + } + ) }, + /** + * @param {any} req + * @param {any} res + * @param {any} next + */ unlearn(req, res, next) { const { body } = parseReq(req, unlearnSchema) const { word } = body const userId = SessionManager.getLoggedInUserId(req.session) - LearnedWordsManager.unlearnWord(userId, word, err => { - if (err) return next(err) - res.sendStatus(204) - }) + LearnedWordsManager.unlearnWord( + userId, + word, + /** @param {any} err */ err => { + if (err) return next(err) + res.sendStatus(204) + } + ) }, } diff --git a/services/web/app/src/Features/Subscription/AiHelper.mjs b/services/web/app/src/Features/Subscription/AiHelper.mjs index 0676c2f420..9d4d6d0a8a 100644 --- a/services/web/app/src/Features/Subscription/AiHelper.mjs +++ b/services/web/app/src/Features/Subscription/AiHelper.mjs @@ -22,7 +22,7 @@ export function isStandaloneAiAddOnPlanCode(planCode) { /** * Returns whether subscription change will have have the ai bundle once the change is processed * - * @param {Object} subscriptionChange The subscription change object coming from payment provider + * @param {Record} subscriptionChange The subscription change object coming from payment provider * type should be PaymentProviderSubscriptionChange but if imported here, it creates a circular dependency * TODO: fix this when moved to es modules * @@ -31,7 +31,9 @@ export function isStandaloneAiAddOnPlanCode(planCode) { export function subscriptionChangeIsAiAssistUpgrade(subscriptionChange) { return Boolean( isStandaloneAiAddOnPlanCode(subscriptionChange.nextPlanCode) || - subscriptionChange.nextAddOns?.some(addOn => addOn.code === AI_ADD_ON_CODE) + subscriptionChange.nextAddOns?.some( + /** @param {any} addOn */ addOn => addOn.code === AI_ADD_ON_CODE + ) ) } diff --git a/services/web/app/src/Features/Subscription/Errors.mjs b/services/web/app/src/Features/Subscription/Errors.mjs index aac4b94fa7..aba8182350 100644 --- a/services/web/app/src/Features/Subscription/Errors.mjs +++ b/services/web/app/src/Features/Subscription/Errors.mjs @@ -5,6 +5,9 @@ import Errors from '../Errors/Errors.js' import OError from '@overleaf/o-error' export class RecurlyTransactionError extends Errors.BackwardCompatibleError { + /** + * @param {any} options + */ constructor(options) { super({ message: 'Unknown transaction error', diff --git a/services/web/app/src/Features/Subscription/LimitationsManager.mjs b/services/web/app/src/Features/Subscription/LimitationsManager.mjs index bd33ee4234..4033a090c3 100644 --- a/services/web/app/src/Features/Subscription/LimitationsManager.mjs +++ b/services/web/app/src/Features/Subscription/LimitationsManager.mjs @@ -11,6 +11,9 @@ import CollaboratorsInvitesGetter from '../Collaborators/CollaboratorsInviteGett import PrivilegeLevels from '../Authorization/PrivilegeLevels.mjs' import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils' +/** + * @param {any} projectId + */ async function allowedNumberOfCollaboratorsInProject(projectId) { const project = await ProjectGetter.promises.getProject(projectId, { owner_ref: true, @@ -18,15 +21,21 @@ async function allowedNumberOfCollaboratorsInProject(projectId) { return await allowedNumberOfCollaboratorsForUser(project.owner_ref) } +/** + * @param {any} userId + */ async function allowedNumberOfCollaboratorsForUser(userId) { const user = await UserGetter.promises.getUser(userId, { features: 1 }) - if (user.features && user.features.collaborators) { + if (user?.features?.collaborators) { return user.features.collaborators } else { return Settings.defaultFeatures.collaborators } } +/** + * @param {any} projectId + */ async function canAcceptEditCollaboratorInvite(projectId) { const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId) if (allowedNumber < 0) { @@ -39,6 +48,10 @@ async function canAcceptEditCollaboratorInvite(projectId) { return currentEditors + 1 <= allowedNumber } +/** + * @param {any} projectId + * @param {any} numberOfNewEditCollaborators + */ async function canAddXEditCollaborators( projectId, numberOfNewEditCollaborators @@ -106,6 +119,9 @@ async function canChangeCollaboratorPrivilegeLevel( return slotsTaken + inviteCount < allowedNumber } +/** + * @param {any} user + */ async function hasPaidSubscription(user) { const { hasSubscription, subscription } = await userHasSubscription(user) const { isMember } = await userIsMemberOfGroupSubscription(user) @@ -115,11 +131,17 @@ async function hasPaidSubscription(user) { } } +/** + * @param {any} user + */ // alias for backward-compatibility with modules. Use `haspaidsubscription` instead async function userHasSubscriptionOrIsGroupMember(user) { return await hasPaidSubscription(user) } +/** + * @param {any} user + */ async function userHasSubscription(user) { const subscription = await SubscriptionLocator.promises.getUsersSubscription( user._id @@ -140,12 +162,18 @@ async function userHasSubscription(user) { } } +/** + * @param {any} user + */ async function userIsMemberOfGroupSubscription(user) { const subscriptions = (await SubscriptionLocator.promises.getMemberSubscriptions(user._id)) || [] return { isMember: subscriptions.length > 0, subscriptions } } +/** + * @param {any} subscription + */ function teamHasReachedMemberLimit(subscription) { const currentTotal = (subscription.member_ids || []).length + @@ -155,6 +183,10 @@ function teamHasReachedMemberLimit(subscription) { return currentTotal >= subscription.membersLimit } +/** + * @param {any} subscriptionId + * @param {any} callback + */ async function hasGroupMembersLimitReached(subscriptionId, callback) { const subscription = await SubscriptionLocator.promises.getSubscription(subscriptionId) diff --git a/services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs b/services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs index 52d25b5018..fe7727eff4 100644 --- a/services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs +++ b/services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs @@ -601,6 +601,9 @@ export class PaymentProviderSubscriptionChange { this.total = this.subtotal + this.tax } + /** + * @param {any} addOnCode + */ getAddOn(addOnCode) { return this.nextAddOns.find(addOn => addOn.code === addOnCode) } diff --git a/services/web/app/src/Features/Subscription/PlansLocator.mjs b/services/web/app/src/Features/Subscription/PlansLocator.mjs index d2e45c033f..281fbb329f 100644 --- a/services/web/app/src/Features/Subscription/PlansLocator.mjs +++ b/services/web/app/src/Features/Subscription/PlansLocator.mjs @@ -14,20 +14,22 @@ import logger from '@overleaf/logger' */ function ensurePlansAreSetupCorrectly() { - Settings.plans.forEach(plan => { - if (typeof plan.price_in_cents !== 'number') { - logger.fatal({ plan }, 'missing price on plan') - process.exit(1) + Settings.plans.forEach( + /** @param {any} plan */ plan => { + if (typeof plan.price_in_cents !== 'number') { + logger.fatal({ plan }, 'missing price on plan') + process.exit(1) + } + if (plan.price) { + logger.fatal({ plan }, 'unclear price attribute on plan') + process.exit(1) + } + if (plan.price_in_unit) { + logger.fatal({ plan }, 'deprecated price_in_unit attribute on plan') + process.exit(1) + } } - if (plan.price) { - logger.fatal({ plan }, 'unclear price attribute on plan') - process.exit(1) - } - if (plan.price_in_unit) { - logger.fatal({ plan }, 'deprecated price_in_unit attribute on plan') - process.exit(1) - } - }) + ) } /** diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.mjs b/services/web/app/src/Features/Subscription/RecurlyClient.mjs index ab1b9f6dea..f07a361cc8 100644 --- a/services/web/app/src/Features/Subscription/RecurlyClient.mjs +++ b/services/web/app/src/Features/Subscription/RecurlyClient.mjs @@ -36,7 +36,7 @@ import _ from 'lodash' class RecurlyClientWithErrorHandling extends recurly.Client { /** - * @param {import('recurly/lib/recurly/Http').Response} response + * @param {any} response * @return {Error | null} * @private */ @@ -78,6 +78,9 @@ async function getAccountForUserId(userId) { } } +/** + * @param {any} userId + */ async function createAccountForUserId(userId) { const user = await UserGetter.promises.getUser(userId, { _id: 1, @@ -345,6 +348,7 @@ async function _reapplyPendingChangeAfterImmediateUpdate( ) // Merge: start with pending add-ons, add any new add-ons from immediate update + /** @type {Record} */ const mergedAddOns = { ...preUpdatePendingAddOns } for (const [code, details] of Object.entries(postUpdateAddOns)) { // include any add-ons that were added via immediate update just now @@ -446,20 +450,32 @@ async function previewSubscriptionChange(changeRequest) { } } +/** + * @param {any} subscriptionId + */ async function removeSubscriptionChange(subscriptionId) { const removed = await client.removeSubscriptionChange(subscriptionId) logger.debug({ subscriptionId }, 'removed pending subscription change') return removed } +/** + * @param {any} subscriptionUuid + */ async function removeSubscriptionChangeByUuid(subscriptionUuid) { return await removeSubscriptionChange('uuid-' + subscriptionUuid) } +/** + * @param {any} subscriptionUuid + */ async function reactivateSubscriptionByUuid(subscriptionUuid) { return await client.reactivateSubscription('uuid-' + subscriptionUuid) } +/** + * @param {any} subscriptionUuid + */ async function cancelSubscriptionByUuid(subscriptionUuid) { try { return await client.cancelSubscription('uuid-' + subscriptionUuid) @@ -493,12 +509,19 @@ async function cancelSubscriptionByUuid(subscriptionUuid) { } } +/** + * @param {any} subscriptionUuid + * @param {any} pauseCycles + */ async function pauseSubscriptionByUuid(subscriptionUuid, pauseCycles) { return await client.pauseSubscription('uuid-' + subscriptionUuid, { remainingPauseCycles: pauseCycles, }) } +/** + * @param {any} subscriptionUuid + */ async function resumeSubscriptionByUuid(subscriptionUuid) { return await client.resumeSubscription('uuid-' + subscriptionUuid) } @@ -552,6 +575,9 @@ async function getPlan(planCode) { return planFromApi(plan) } +/** + * @param {any} subscription + */ function subscriptionIsCanceledOrExpired(subscription) { const state = subscription?.recurlyStatus?.state return state === 'canceled' || state === 'expired' @@ -884,6 +910,9 @@ function subscriptionUpdateRequestToApi(updateRequest) { return requestBody } +/** + * @param {any} subscriptionUuid + */ async function terminateSubscriptionByUuid(subscriptionUuid) { const subscription = await client.terminateSubscription( 'uuid-' + subscriptionUuid, diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.mjs b/services/web/app/src/Features/Subscription/RecurlyWrapper.mjs index d26a6ea54b..9e90df0ceb 100644 --- a/services/web/app/src/Features/Subscription/RecurlyWrapper.mjs +++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.mjs @@ -14,8 +14,8 @@ import RecurlyMetrics from './RecurlyMetrics.mjs' /** * Updates the email address of a Recurly account * - * @param userId - * @param newAccountEmail - the new email address to set for the Recurly account + * @param {any} userId + * @param {any} newAccountEmail - the new email address to set for the Recurly account */ async function updateAccountEmailAddress(userId, newAccountEmail) { const data = { @@ -903,11 +903,10 @@ const RecurlyWrapper = { getSubscription: callbackify(promises.getSubscription), getSubscriptions: callbackify(promises.getSubscriptions), updateAccountEmailAddress: callbackify(promises.updateAccountEmailAddress), -} - -RecurlyWrapper.promises = { - ...promises, - updateAccountEmailAddress, + promises: { + ...promises, + updateAccountEmailAddress, + }, } export default RecurlyWrapper diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index a5c197f6c9..b8d15c305d 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -54,7 +54,7 @@ const SUBSCRIPTION_PAUSED_REDIRECT_PATH = /** * Check if a Stripe subscription is currently paused - * @param {Object} subscription - The subscription object + * @param {Record} subscription - The subscription object * @returns {Promise} */ async function _checkStripeSubscriptionPauseStatus(subscription) { @@ -78,7 +78,7 @@ async function _checkStripeSubscriptionPauseStatus(subscription) { /** * Check if a Recurly subscription is currently paused - * @param {Object} subscription - The subscription object + * @param {Record} subscription - The subscription object * @returns {Promise} */ async function _checkRecurlySubscriptionPauseStatus(subscription) { @@ -102,7 +102,7 @@ async function _checkRecurlySubscriptionPauseStatus(subscription) { } /** Check if a user's subscription is manual or custom - * @param {Object} user - The user object + * @param {Record} user - The user object * @returns {Promise} */ async function _isManualOrCustomSubscription(user) { @@ -120,7 +120,7 @@ async function _isManualOrCustomSubscription(user) { /** * Check if a user's subscription is currently paused - * @param {Object} user - The user object + * @param {Record} user - The user object * @returns {Promise<{isPaused: boolean, redirectPath?: string}>} */ async function checkSubscriptionPauseStatus(user) { @@ -176,6 +176,10 @@ function formatGroupPlansDataForDash() { } } +/** + * @param {any} req + * @param {any} res + */ async function userSubscriptionPage(req, res) { const user = SessionManager.getSessionUser(req.session) await SplitTestHandler.promises.getAssignment(req, res, 'sharing-updates') @@ -242,6 +246,7 @@ async function userSubscriptionPage(req, res) { try { const managedGroups = await async.filter( managedGroupSubscriptions || [], + /** @param {any} subscription */ async subscription => { const managedUsersResults = await Modules.promises.hooks.fire( 'hasManagedUsersFeature', @@ -261,8 +266,8 @@ async function userSubscriptionPage(req, res) { ) } ) - groupSettingsEnabledFor = managedGroups.map(subscription => - subscription._id.toString() + groupSettingsEnabledFor = managedGroups.map( + (/** @type {any} */ subscription) => subscription._id.toString() ) } catch (error) { logger.error( @@ -275,7 +280,7 @@ async function userSubscriptionPage(req, res) { try { const managedGroups = await async.filter( managedGroupSubscriptions || [], - async subscription => { + async (/** @type {any} */ subscription) => { const managedUsersResults = await Modules.promises.hooks.fire( 'hasManagedUsersFeatureOnNonProfessionalPlan', subscription @@ -296,8 +301,8 @@ async function userSubscriptionPage(req, res) { ) } ) - groupSettingsAdvertisedFor = managedGroups.map(subscription => - subscription._id.toString() + groupSettingsAdvertisedFor = managedGroups.map( + (/** @type {any} */ subscription) => subscription._id.toString() ) } catch (error) { logger.error( @@ -339,6 +344,10 @@ async function userSubscriptionPage(req, res) { res.render('subscriptions/dashboard-react', data) } +/** + * @param {any} req + * @param {any} res + */ async function successfulSubscription(req, res) { const user = SessionManager.getSessionUser(req.session) if (!user) { @@ -382,6 +391,11 @@ const pauseSubscriptionSchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function pauseSubscription(req, res, next) { const user = SessionManager.getSessionUser(req.session) const { params } = parseReq(req, pauseSubscriptionSchema) @@ -425,6 +439,11 @@ async function pauseSubscription(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function resumeSubscription(req, res, next) { const user = SessionManager.getSessionUser(req.session) logger.debug({ userId: user._id }, `resuming subscription`) @@ -441,6 +460,11 @@ async function resumeSubscription(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function cancelSubscription(req, res, next) { const user = SessionManager.getSessionUser(req.session) logger.debug({ userId: user._id }, 'canceling subscription') @@ -456,6 +480,9 @@ async function cancelSubscription(req, res, next) { } /** + * @param {any} req + * @param {any} res + * @param {any} next * @returns {Promise} */ async function canceledSubscription(req, res, next) { @@ -467,20 +494,32 @@ async function canceledSubscription(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function cancelV1Subscription(req, res, next) { const userId = SessionManager.getLoggedInUserId(req.session) logger.debug({ userId }, 'canceling v1 subscription') - V1SubscriptionManager.cancelV1Subscription(userId, function (err) { - if (err) { - OError.tag(err, 'something went wrong canceling v1 subscription', { - userId, - }) - return next(err) + V1SubscriptionManager.cancelV1Subscription( + userId, + /** @param {any} err */ function (err) { + if (err) { + OError.tag(err, 'something went wrong canceling v1 subscription', { + userId, + }) + return next(err) + } + res.redirect('/user/subscription') } - res.redirect('/user/subscription') - }) + ) } +/** + * @param {any} req + * @param {any} res + */ async function previewAddonPurchase(req, res) { const user = SessionManager.getSessionUser(req.session) const userId = user._id @@ -600,6 +639,11 @@ const purchaseAddonSchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function purchaseAddon(req, res, next) { const user = SessionManager.getSessionUser(req.session) const { params } = parseReq(req, purchaseAddonSchema) @@ -642,22 +686,22 @@ async function purchaseAddon(req, res, next) { ) return res.status(402).json({ message: 'Payment action required', - clientSecret: err.info.clientSecret, - publicKey: err.info.publicKey, + clientSecret: /** @type {any} */ (err).info.clientSecret, + publicKey: /** @type {any} */ (err).info.publicKey, }) } else if (err instanceof PaymentFailedError) { logger.debug( { userId: user._id, - reason: err.info.reason, - adviceCode: err.info.adviceCode, + reason: /** @type {any} */ (err).info.reason, + adviceCode: /** @type {any} */ (err).info.adviceCode, }, 'Payment failed for transaction' ) return res.status(402).json({ message: 'Payment failed', - reason: err.info.reason, - adviceCode: err.info.adviceCode, + reason: /** @type {any} */ (err).info.reason, + adviceCode: /** @type {any} */ (err).info.adviceCode, }) } else if (err instanceof MultiplePendingChangesError) { logger.warn( @@ -695,6 +739,11 @@ const removeAddonSchema = z.object({ }), }) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function removeAddon(req, res, next) { const user = SessionManager.getSessionUser(req.session) const { params } = parseReq(req, removeAddonSchema) @@ -749,6 +798,8 @@ const reactivateAddonSchema = z.object({ * Reactivate an add-on pending cancellation * * This "cancels" the cancellation. + * @param {any} req + * @param {any} res */ async function reactivateAddon(req, res) { const user = SessionManager.getSessionUser(req.session) @@ -776,6 +827,11 @@ async function reactivateAddon(req, res) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function previewSubscription(req, res, next) { const planCode = req.query.planCode if (!planCode) { @@ -822,24 +878,37 @@ async function previewSubscription(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function cancelPendingSubscriptionChange(req, res, next) { const user = SessionManager.getSessionUser(req.session) logger.debug({ userId: user._id }, 'canceling pending subscription change') - SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) { - if (err) { - OError.tag( - err, - 'something went wrong canceling pending subscription change', - { - user_id: user._id, - } - ) - return next(err) + SubscriptionHandler.cancelPendingSubscriptionChange( + user, + /** @param {any} err */ function (err) { + if (err) { + OError.tag( + err, + 'something went wrong canceling pending subscription change', + { + user_id: user._id, + } + ) + return next(err) + } + res.redirect('/user/subscription') } - res.redirect('/user/subscription') - }) + ) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function updateAccountEmailAddress(req, res, next) { const user = SessionManager.getSessionUser(req.session) try { @@ -854,6 +923,11 @@ async function updateAccountEmailAddress(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function reactivateSubscription(req, res, next) { const user = SessionManager.getSessionUser(req.session) logger.debug({ userId: user._id }, 'reactivating subscription') @@ -878,6 +952,11 @@ function reactivateSubscription(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function recurlyCallback(req, res, next) { logger.debug({ data: req.body }, 'received recurly callback') const event = Object.keys(req.body)[0] @@ -926,6 +1005,10 @@ function recurlyCallback(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + */ async function extendTrial(req, res) { const user = SessionManager.getSessionUser(req.session) const { subscription } = @@ -951,20 +1034,36 @@ async function extendTrial(req, res) { res.sendStatus(200) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function recurlyNotificationParser(req, res, next) { let xml = '' - req.on('data', chunk => (xml += chunk)) + req.on('data', /** @param {any} chunk */ chunk => (xml += chunk)) req.on('end', () => - RecurlyWrapper._parseXml(xml, function (error, body) { - if (error) { - return next(error) + RecurlyWrapper._parseXml( + xml, + /** + * @param {any} error + * @param {any} body + */ + function (error, body) { + if (error) { + return next(error) + } + req.body = body + next() } - req.body = body - next() - }) + ) ) } +/** + * @param {any} req + * @param {any} res + */ async function refreshUserFeatures(req, res) { const { user_id: userId } = req.params await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test') @@ -972,6 +1071,8 @@ async function refreshUserFeatures(req, res) { } /** + * @param {any} req + * @param {any} res * @returns {Promise<{currency: CurrencyCode, recommendedCurrency: CurrencyCode, countryCode: string|undefined}>} */ async function getRecommendedCurrency(req, res) { @@ -1002,6 +1103,10 @@ async function getRecommendedCurrency(req, res) { } } +/** + * @param {any} req + * @param {any} res + */ async function getLatamCountryBannerDetails(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) let ip = req.ip diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.mjs b/services/web/app/src/Features/Subscription/SubscriptionHandler.mjs index 7125e7f7bc..1da9ba202c 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHandler.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.mjs @@ -17,6 +17,9 @@ import { AI_ADD_ON_CODE } from './AiHelper.mjs' * @import { PaymentProviderSubscriptionChange } from './PaymentProviderEntities.mjs' */ +/** + * @param {any} userId + */ async function validateNoSubscriptionInRecurly(userId) { let subscriptions = await RecurlyWrapper.promises.listAccountActiveSubscriptions(userId) @@ -37,6 +40,11 @@ async function validateNoSubscriptionInRecurly(userId) { return true } +/** + * @param {any} user + * @param {any} subscriptionDetails + * @param {any} recurlyTokenIds + */ async function createSubscription(user, subscriptionDetails, recurlyTokenIds) { const valid = await validateNoSubscriptionInRecurly(user._id) @@ -82,7 +90,8 @@ async function previewSubscriptionChange(userId, planCode) { /** * @param user - * @param planCode + * @param {any} user + * @param {any} planCode */ async function updateSubscription(user, planCode) { let hasSubscription = false @@ -115,7 +124,7 @@ async function updateSubscription(user, planCode) { } /** - * @param user + * @param {any} user */ async function cancelPendingSubscriptionChange(user) { const { hasSubscription, subscription } = @@ -151,7 +160,7 @@ async function cancelPendingSubscriptionChange(user) { /** * Send cancellation email to user with split test for AI Assist addon - * @param user + * @param {any} user */ async function _sendCancellationEmail(user) { const emailOpts = { @@ -174,7 +183,7 @@ async function _sendCancellationEmail(user) { } /** - * @param user + * @param {any} user */ async function cancelSubscription(user) { const { hasSubscription, subscription } = @@ -187,7 +196,7 @@ async function cancelSubscription(user) { } /** - * @param user + * @param {any} user */ async function reactivateSubscription(user) { try { @@ -220,8 +229,8 @@ async function reactivateSubscription(user) { } /** - * @param recurlySubscription - * @param requesterData + * @param {any} recurlySubscription + * @param {any} requesterData */ async function syncSubscription(recurlySubscription, requesterData) { const storedSubscription = await RecurlyWrapper.promises.getSubscription( @@ -250,7 +259,7 @@ async function syncSubscription(recurlySubscription, requesterData) { * This is used because Recurly doesn't always attempt collection of paast due * invoices after Paypal billing info were updated. * - * @param recurlyAccountCode + * @param {any} recurlyAccountCode */ async function attemptPaypalInvoiceCollection(recurlyAccountCode) { const billingInfo = @@ -274,6 +283,10 @@ async function attemptPaypalInvoiceCollection(recurlyAccountCode) { ) } +/** + * @param {any} subscription + * @param {any} daysToExtend + */ async function extendTrial(subscription, daysToExtend) { await Modules.promises.hooks.fire('extendTrial', subscription, daysToExtend) } @@ -313,7 +326,7 @@ async function purchaseAddon(userId, addOnCode, quantity) { /** * Cancels an add-on for a user * - * @param user + * @param {any} user * @param {string} addOnCode */ async function removeAddon(user, addOnCode) { @@ -334,6 +347,10 @@ async function reactivateAddon(userId, addOnCode) { await Modules.promises.hooks.fire('reactivateAddOn', userId, addOnCode) } +/** + * @param {any} user + * @param {any} pauseCycles + */ async function pauseSubscription(user, pauseCycles) { // only allow pausing on monthly plans not in a trial const { subscription } = @@ -367,8 +384,9 @@ async function pauseSubscription(user, pauseCycles) { pauseCycles ) } - -async function resumeSubscription(user) { +/** + * @param {any} user + */ async function resumeSubscription(user) { const { subscription } = await LimitationsManager.promises.userHasSubscription(user) if ( diff --git a/services/web/app/src/Features/Subscription/SubscriptionHelper.mjs b/services/web/app/src/Features/Subscription/SubscriptionHelper.mjs index ca67df6e15..5844810053 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHelper.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionHelper.mjs @@ -12,8 +12,8 @@ const MILLISECONDS = 1_000 * This function checks if a subscription should transition between 'active' and 'paused' * states based on the current time and pause period metadata. * - * @param {Object} subscription - The MongoDB subscription document - * @returns {Promise} - The updated subscription document with recomputed state + * @param {any} subscription - The MongoDB subscription document + * @returns {Promise} - The updated subscription document with recomputed state */ async function recomputeSubscriptionState(subscription) { if ( @@ -65,6 +65,9 @@ async function recomputeSubscriptionState(subscription) { /** * If the user changes to a less expensive plan, we shouldn't apply the change immediately. * This is to avoid unintended/artifical credits on users Recurly accounts. + * @param {any} oldPlan + * @param {any} newPlan + * @param {any} isInTrial */ function shouldPlanChangeAtTermEnd(oldPlan, newPlan, isInTrial) { if (isInTrial) { @@ -138,6 +141,9 @@ function generateInitialLocalizedGroupPrice(recommendedCurrency, locale) { } } +/** + * @param {any} subscription + */ function isPaidSubscription(subscription) { const hasRecurlySubscription = subscription?.recurlySubscription_id && @@ -148,6 +154,9 @@ function isPaidSubscription(subscription) { return !!(subscription && (hasRecurlySubscription || hasStripeSubscription)) } +/** + * @param {any} subscription + */ function isIndividualActivePaidSubscription(subscription) { return ( isPaidSubscription(subscription) && @@ -157,6 +166,9 @@ function isIndividualActivePaidSubscription(subscription) { ) } +/** + * @param {any} subscription + */ function getPaymentProviderSubscriptionId(subscription) { if (subscription?.recurlySubscription_id) { return subscription.recurlySubscription_id @@ -167,6 +179,9 @@ function getPaymentProviderSubscriptionId(subscription) { return null } +/** + * @param {any} subscription + */ function getPaidSubscriptionState(subscription) { if (subscription?.recurlyStatus?.state) { return subscription.recurlyStatus.state @@ -177,6 +192,9 @@ function getPaidSubscriptionState(subscription) { return null } +/** + * @param {any} subscription + */ function getSubscriptionTrialStartedAt(subscription) { if (subscription?.recurlyStatus?.trialStartedAt) { return subscription.recurlyStatus?.trialStartedAt @@ -184,6 +202,9 @@ function getSubscriptionTrialStartedAt(subscription) { return subscription?.paymentProvider?.trialStartedAt } +/** + * @param {any} subscription + */ function getSubscriptionTrialEndsAt(subscription) { if (subscription?.recurlyStatus?.trialEndsAt) { return subscription.recurlyStatus?.trialEndsAt @@ -191,6 +212,9 @@ function getSubscriptionTrialEndsAt(subscription) { return subscription?.paymentProvider?.trialEndsAt } +/** + * @param {any} trialEndsAt + */ function isInTrial(trialEndsAt) { if (!trialEndsAt) { return false diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs index 6fc2fa6024..340dd138d4 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs @@ -25,6 +25,11 @@ const rateLimiters = { }), } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function createInvite(req, res, next) { const teamManagerId = SessionManager.getLoggedInUserId(req.session) const subscription = req.entity @@ -70,6 +75,11 @@ async function createInvite(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function viewInvite(req, res, next) { const { token } = req.params const sessionUser = SessionManager.getSessionUser(req.session) @@ -186,6 +196,11 @@ async function viewInvite(req, res, next) { } } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function viewInvites(req, res, next) { const user = SessionManager.getSessionUser(req.session) const groupSubscriptions = @@ -201,6 +216,11 @@ async function viewInvites(req, res, next) { }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function acceptInvite(req, res, next) { const { token } = req.params const userId = SessionManager.getLoggedInUserId(req.session) @@ -232,6 +252,11 @@ async function acceptInvite(req, res, next) { res.json({ groupSSOActive }) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function revokeInvite(req, res, next) { const subscription = req.entity const email = EmailHelper.parseEmail(req.params.email) @@ -244,6 +269,10 @@ function revokeInvite(req, res, next) { teamManagerId, subscription, email, + /** + * @param {any} err + * @param {any} results + */ function (err, results) { if (err) { return next(err) @@ -253,6 +282,11 @@ function revokeInvite(req, res, next) { ) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function resendInvite(req, res, next) { const { entity: subscription } = req const userEmail = EmailHelper.parseEmail(req.body.email) diff --git a/services/web/app/src/Features/Uploads/FileTypeManager.mjs b/services/web/app/src/Features/Uploads/FileTypeManager.mjs index bb489bcd92..2b0b0a2b73 100644 --- a/services/web/app/src/Features/Uploads/FileTypeManager.mjs +++ b/services/web/app/src/Features/Uploads/FileTypeManager.mjs @@ -17,12 +17,17 @@ const FILE_IGNORE_MATCHER = new Minimatch(Settings.fileIgnorePattern, { dot: true, }) -const TEXT_EXTENSIONS = new Set(Settings.textExtensions.map(ext => `.${ext}`)) +const TEXT_EXTENSIONS = new Set( + Settings.textExtensions.map((/** @type {string} */ ext) => `.${ext}`) +) const EDITABLE_FILENAMES = Settings.editableFilenames // allow 3 bytes for every character const MAX_TEXT_FILE_SIZE = 3 * Settings.max_doc_length +/** + * @param {string} path + */ async function isDirectory(path) { const stats = await fs.stat(path) return stats.isDirectory() @@ -86,11 +91,17 @@ async function getType(name, fsPath, existingFileType) { } } +/** + * @param {string} path + */ function shouldIgnore(path) { // use minimatch file matching to check if the path should be ignored return FILE_IGNORE_MATCHER.match(path) } +/** + * @param {string} filename + */ function _isTextFilename(filename) { const basename = Path.basename(filename) const extension = Path.extname(filename).toLowerCase() @@ -100,6 +111,9 @@ function _isTextFilename(filename) { ) } +/** + * @param {Buffer} bytes + */ function _detectEncoding(bytes) { if (isUtf8(bytes)) { return 'utf-8' diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs index cc02e5bce7..e360aaf768 100644 --- a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs @@ -28,6 +28,11 @@ const upload = multer( ) ) +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function uploadProject(req, res, next) { const timer = new metrics.Timer('project-upload') const userId = SessionManager.getLoggedInUserId(req.session) @@ -63,6 +68,11 @@ function uploadProject(req, res, next) { ) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ async function uploadFile(req, res, next) { const timer = new metrics.Timer('file-upload') const name = req.body.name @@ -156,27 +166,36 @@ async function uploadFile(req, res, next) { ) } +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function multerMiddleware(req, res, next) { if (upload == null) { return res .status(500) .json({ success: false, error: req.i18n.translate('upload_failed') }) } - return upload.single('qqfile')(req, res, function (err) { - if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { - return res - .status(422) - .json({ success: false, error: req.i18n.translate('file_too_large') }) + return upload.single('qqfile')( + req, + res, + /** @param {any} err */ function (err) { + if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { + return res + .status(422) + .json({ success: false, error: req.i18n.translate('file_too_large') }) + } + if (err) return next(err) + if (!req.file?.path) { + logger.info({ req }, 'missing req.file.path on upload') + return res + .status(400) + .json({ success: false, error: 'invalid_upload_request' }) + } + next() } - if (err) return next(err) - if (!req.file?.path) { - logger.info({ req }, 'missing req.file.path on upload') - return res - .status(400) - .json({ success: false, error: 'invalid_upload_request' }) - } - next() - }) + ) } export default { diff --git a/services/web/app/src/Features/User/UserGetter.mjs b/services/web/app/src/Features/User/UserGetter.mjs index 71b7312746..4032f4f057 100644 --- a/services/web/app/src/Features/User/UserGetter.mjs +++ b/services/web/app/src/Features/User/UserGetter.mjs @@ -274,6 +274,24 @@ const UserGetter = { // check for duplicate email address. This is also enforced at the DB level ensureUniqueEmailAddress: callbackify(ensureUniqueEmailAddress), getWritefullData: callbackify(getWritefullData), + + promises: { + getSsoUsersAtInstitution, + getUser, + getUserFeatures, + getUserEmail, + getUserFullEmails, + getUserConfirmedEmails, + getUserByMainEmail, + getUserByAnyEmail, + getUsersByAnyConfirmedEmail, + getUsersByV1Ids, + getUsersByHostname, + getInstitutionUsersByHostname, + getUsers, + ensureUniqueEmailAddress, + getWritefullData, + }, } const decorateFullEmails = ( @@ -352,22 +370,4 @@ const decorateFullEmails = ( return emailsData } -UserGetter.promises = { - getSsoUsersAtInstitution, - getUser, - getUserFeatures, - getUserEmail, - getUserFullEmails, - getUserConfirmedEmails, - getUserByMainEmail, - getUserByAnyEmail, - getUsersByAnyConfirmedEmail, - getUsersByV1Ids, - getUsersByHostname, - getInstitutionUsersByHostname, - getUsers, - ensureUniqueEmailAddress, - getWritefullData, -} - export default UserGetter diff --git a/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.mjs b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.mjs index 3eaefd9859..fb558ae44a 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.mjs +++ b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.mjs @@ -36,7 +36,12 @@ const UserMembershipMiddleware = { requireEntity(), ], - requireEntityAccess: ({ entityName, adminCapability }) => [ + requireEntityAccess: ( + /** @type {{ entityName: any; adminCapability?: any }} */ { + entityName, + adminCapability, + } + ) => [ AuthenticationController.requireLogin(), fetchEntityConfig(entityName), fetchEntity(), @@ -50,7 +55,7 @@ const UserMembershipMiddleware = { ), ], - requireEntityAccessOrAdminAccess: entityName => [ + requireEntityAccessOrAdminAccess: (/** @type {any} */ entityName) => [ AuthenticationController.requireLogin(), fetchEntityConfig(entityName), fetchEntity(), @@ -61,7 +66,7 @@ const UserMembershipMiddleware = { ]), ], - requireGroupMemberManagement: entityName => [ + requireGroupMemberManagement: (/** @type {any} */ entityName) => [ AuthenticationController.requireLogin(), fetchEntityConfig(entityName), fetchEntity(), @@ -223,10 +228,20 @@ const UserMembershipMiddleware = { export default UserMembershipMiddleware -// fetch entity config and set it in the request +/** + * fetch entity config and set it in the request + * + * @param {any} entityName + */ function fetchEntityConfig(entityName) { - return (req, res, next) => { - const entityConfig = EntityConfigs[entityName] + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { + const entityConfig = /** @type {Record} */ (EntityConfigs)[ + entityName + ] req.entityName = entityName req.entityConfig = entityConfig next() @@ -271,6 +286,11 @@ const fetchEntitySchema = z.discriminatedUnion('entityName', [ // `req.params.id` // - the entity name is in `req.query.resource_type` and is used to find the // require middleware depending on the entity name +/** + * @param {any} req + * @param {any} res + * @param {any} next + */ function requireGraphAccess(req, res, next) { const entityName = req.query.resource_type if (!entityName) { @@ -279,8 +299,9 @@ function requireGraphAccess(req, res, next) { const middleWareName = entityName.charAt(0).toUpperCase() + entityName.slice(1) - const middlewares = - UserMembershipMiddleware[`require${middleWareName}MetricsAccess`] + const middlewares = /** @type {Record} */ ( + UserMembershipMiddleware + )[`require${middleWareName}MetricsAccess`] if (!middlewares) { return HttpErrorHandler.notFound( req, @@ -305,19 +326,29 @@ function requireGraphAccess(req, res, next) { // fetch the entity with id and config, and set it in the request function fetchEntity() { - return expressify(async (req, res, next) => { - const { params } = parseReq(req, fetchEntitySchema) - req.entity = - await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck( - params.id, - req.entityConfig - ) - next() - }) + return expressify( + async ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { + const { params } = parseReq(req, fetchEntitySchema) + req.entity = + await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck( + params.id, + req.entityConfig + ) + next() + } + ) } function fetchPublisherFromTemplate() { - return (req, res, next) => { + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { if (req.template.brand.slug) { // set the id as the publisher's id as it's the entity used for access // control @@ -331,7 +362,11 @@ function fetchPublisherFromTemplate() { // ensure an entity was found, or fail with 404 function requireEntity() { - return (req, res, next) => { + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { if (req.entity) { return next() } @@ -342,10 +377,16 @@ function requireEntity() { } } -// ensure an entity was found or redirect to entity creation page if the user -// has permissions to create the entity, or fail with 404 +/** + * ensure an entity was found or redirect to entity creation page if the user + * has permissions to create the entity, or fail with 404 + */ function requireEntityOrCreate() { - return (req, res, next) => { + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { if (req.entity) { return next() } @@ -363,21 +404,31 @@ function requireEntityOrCreate() { // fetch the template from v1, and set it in the request function fetchV1Template() { - return expressify(async (req, res, next) => { - const templateId = req.params.id - const body = await TemplatesManager.promises.fetchFromV1(templateId) - req.template = { - id: body.id, - title: body.title, - brand: body.brand, + return expressify( + async ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { + const templateId = req.params.id + const body = await TemplatesManager.promises.fetchFromV1(templateId) + req.template = { + id: body.id, + title: body.title, + brand: body.brand, + } + next() } - next() - }) + ) } // ensure a template was found, or fail with 404 function requireV1Template() { - return (req, res, next) => { + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { if (req.template.id) { return next() } @@ -386,10 +437,18 @@ function requireV1Template() { } } -// run a serie of synchronous access functions and call `next` if any of the -// retur values is truly. Redirect to restricted otherwise +/** + * run a series of synchronous access functions and call `next` if any of the + * return values is truly. Redirect to restricted otherwise + * + * @param {any} accessFunctions + */ function allowAccessIfAny(accessFunctions) { - return (req, res, next) => { + return ( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {any} */ next + ) => { for (const accessFunction of accessFunctions) { if (accessFunction(req)) { return next() diff --git a/services/web/app/src/infrastructure/AsyncLocalStorage.mjs b/services/web/app/src/infrastructure/AsyncLocalStorage.mjs index eb822e6b9e..476fae74b3 100644 --- a/services/web/app/src/infrastructure/AsyncLocalStorage.mjs +++ b/services/web/app/src/infrastructure/AsyncLocalStorage.mjs @@ -2,8 +2,8 @@ import { AsyncLocalStorage } from 'node:async_hooks' /** - * @typedef {Object} RequestContext - * @property {Object.} [userFullEmails] - Dictionary mapping userId to an array of full emails + * @typedef {Record} RequestContext + * @property {Record} [userFullEmails] - Dictionary mapping userId to an array of full emails */ /** @type {AsyncLocalStorage} */ diff --git a/services/web/app/src/infrastructure/GeoIpLookup.mjs b/services/web/app/src/infrastructure/GeoIpLookup.mjs index 21b09d7cce..ba9d05cba7 100644 --- a/services/web/app/src/infrastructure/GeoIpLookup.mjs +++ b/services/web/app/src/infrastructure/GeoIpLookup.mjs @@ -73,6 +73,9 @@ for (const country of EuroCountries) { currencyMappings[country] = 'EUR' } +/** + * @param {any} currency + */ function isValidCurrencyParam(currency) { if (!currency) { return false @@ -80,6 +83,10 @@ function isValidCurrencyParam(currency) { return validCurrencyParams.includes(currency) } +/** + * @param {any} ip + * @param {any} [callback] + */ async function getDetails(ip, callback) { if (!ip) { return callback(new Error('no ip passed')) @@ -92,6 +99,7 @@ async function getDetails(ip, callback) { } /** + * @param {any} ip * @returns {Promise<{currencyCode: CurrencyCode, countryCode: string|undefined}>} */ async function getCurrencyCode(ip) { diff --git a/services/web/app/src/infrastructure/HttpPermissionsPolicy.mjs b/services/web/app/src/infrastructure/HttpPermissionsPolicy.mjs index 7491871da2..30a4d7c359 100644 --- a/services/web/app/src/infrastructure/HttpPermissionsPolicy.mjs +++ b/services/web/app/src/infrastructure/HttpPermissionsPolicy.mjs @@ -72,11 +72,16 @@ class HttpPermissionsPolicyMiddleware { return policyElements.join(', ') } + /** + * @param {any} req + * @param {any} res + * @param {any} next + */ middleware(req, res, next) { if (this.policy && Settings.useHttpPermissionsPolicy) { const originalRender = res.render - res.render = (...args) => { + res.render = (/** @type {any} */ ...args) => { res.setHeader('Permissions-Policy', this.policy) originalRender.apply(res, args) } diff --git a/services/web/app/src/infrastructure/Modules.mjs b/services/web/app/src/infrastructure/Modules.mjs index 4d35491651..e6b0e251d2 100644 --- a/services/web/app/src/infrastructure/Modules.mjs +++ b/services/web/app/src/infrastructure/Modules.mjs @@ -17,10 +17,12 @@ const MODULE_BASE_PATH = Path.join(import.meta.dirname, '/../../../modules') /** @type {WebModule[]} */ const _modules = [] let _modulesLoaded = false +/** @type {Record} */ const _hooks = {} /** @type {Record} */ const _middleware = {} +/** @type {Record} */ let _viewIncludes = {} async function modules() { @@ -74,6 +76,11 @@ async function loadModulesImpl() { const loadModules = _.memoize(loadModulesImpl) +/** + * @param {any} webRouter + * @param {any} privateApiRouter + * @param {any} publicApiRouter + */ async function applyRouter(webRouter, privateApiRouter, publicApiRouter) { for (const module of await modules()) { if (module.router && module.router.apply) { @@ -82,6 +89,11 @@ async function applyRouter(webRouter, privateApiRouter, publicApiRouter) { } } +/** + * @param {any} webRouter + * @param {any} privateApiRouter + * @param {any} publicApiRouter + */ async function applyNonCsrfRouter( webRouter, privateApiRouter, @@ -107,10 +119,18 @@ async function start() { } } +/** + * @param {any} app + */ function loadViewIncludes(app) { _viewIncludes = Views.compileViewIncludes(app) } +/** + * @param {any} appOrRouter + * @param {any} middlewareName + * @param {any} [options] + */ async function applyMiddleware(appOrRouter, middlewareName, options) { if (!middlewareName) { throw new Error( @@ -118,30 +138,42 @@ async function applyMiddleware(appOrRouter, middlewareName, options) { ) } for (const module of await modules()) { - if (module[middlewareName]) { - module[middlewareName](appOrRouter, options) + /** @type {Record} */ + const typedModule = module + if (typedModule[middlewareName]) { + typedModule[middlewareName](appOrRouter, options) } } } +/** + * @param {any} view + * @param {any} locals + */ function moduleIncludes(view, locals) { const compiledPartials = _viewIncludes[view] || [] let html = '' - for (const compiledPartial of compiledPartials) { + for (const /** @type {any} */ compiledPartial of compiledPartials) { html += compiledPartial(locals) } return html } +/** + * @param {any} view + */ function moduleIncludesAvailable(view) { return (_viewIncludes[view] || []).length > 0 } async function linkedFileAgentsIncludes() { + /** @type {Record} */ const agents = {} for (const module of await modules()) { for (const name in module.linkedFileAgents) { - const agentFunction = module.linkedFileAgents[name] + const agentFunction = /** @type {Record} */ ( + module.linkedFileAgents + )[name] agents[name] = agentFunction() } } @@ -155,12 +187,16 @@ async function attachHooks() { attachHook(hook, method) } for (const hook in hooks || {}) { - const method = hooks[hook] + const method = /** @type {Record} */ (hooks)[hook] attachHook(hook, promisify(method)) } } } +/** + * @param {any} name + * @param {any} method + */ function attachHook(name, method) { if (_hooks[name] == null) { _hooks[name] = [] @@ -172,7 +208,9 @@ async function attachMiddleware() { for (const module of await modules()) { if (module.middleware) { for (const middleware in module.middleware) { - const method = module.middleware[middleware] + const method = /** @type {Record} */ (module.middleware)[ + middleware + ] if (_middleware[middleware] == null) { _middleware[middleware] = [] } @@ -182,6 +220,10 @@ async function attachMiddleware() { } } +/** + * @param {any} name + * @param {...any} args + */ async function fireHook(name, ...args) { // ensure that modules are loaded if we need to fire a hook // this can happen if a script calls a method that fires a hook diff --git a/services/web/app/src/infrastructure/Validation.mjs b/services/web/app/src/infrastructure/Validation.mjs index c0bd79e28d..4f545b4d7c 100644 --- a/services/web/app/src/infrastructure/Validation.mjs +++ b/services/web/app/src/infrastructure/Validation.mjs @@ -11,10 +11,14 @@ import { export { z, zz } from '@overleaf/validation-tools' +/** + * @param {any} req + * @param {any} schema + */ export const parseReq = (req, schema) => { try { return parseReqBase(req, schema) - } catch (err) { + } catch (/** @type {any} */ err) { if (err instanceof ParamsError) { // convert into a NotFoundError that web understands throw new NotFoundError('Not found').withCause(err) diff --git a/services/web/app/src/infrastructure/rate-limiters/FeatureUsageRateLimiter.mjs b/services/web/app/src/infrastructure/rate-limiters/FeatureUsageRateLimiter.mjs index 23bcdbdf9e..78326b7dd3 100644 --- a/services/web/app/src/infrastructure/rate-limiters/FeatureUsageRateLimiter.mjs +++ b/services/web/app/src/infrastructure/rate-limiters/FeatureUsageRateLimiter.mjs @@ -88,7 +88,12 @@ export default class FeatureUsageRateLimiter { upsert: true, } ).exec() - const featureUsage = featureUsages.features?.[this.featureName] ?? {} + + const featureUsage = + /** @type {Record} */ (featureUsages.features ?? {})[ + this.featureName + ] ?? {} + setRateLimitHeaders(res, featureUsage, allowance) this._checkRateLimit(featureUsage, allowance) } @@ -118,7 +123,10 @@ export default class FeatureUsageRateLimiter { } ).exec() - const featureUsage = featureUsages.features?.[this.featureName] ?? {} + const featureUsage = + /** @type {Record} */ (featureUsages.features ?? {})[ + this.featureName + ] ?? {} setRateLimitHeaders(res, featureUsage, allowance) } @@ -129,7 +137,10 @@ export default class FeatureUsageRateLimiter { async getRemainingFeatureUses(userId) { const allowance = await this._getAllowance(userId) const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec() - const featureUsage = reportedUsage?.features?.[this.featureName] ?? {} + const featureUsage = + /** @type {Record} */ (reportedUsage?.features ?? {})[ + this.featureName + ] ?? {} const periodStart = featureUsage.periodStart ?? new Date() const usage = featureUsage.usage ?? 0 const usesLeft = allowance - usage diff --git a/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs b/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs index 9c045ca8af..79afdf75f7 100644 --- a/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs +++ b/services/web/app/src/infrastructure/rate-limiters/TokenUsageRateLimiter.mjs @@ -57,9 +57,15 @@ export default class TokenUsageRateLimiter { throw new Error('_getAllowance must be implemented by subclasses') } + /** + * @param {any} userId + * @param {any} res + * @param {any} amount + */ async recordUsage(userId, res, amount) { const allowance = await this._getAllowance(userId) + /** @type {any} */ const featureUsages = await UserFeatureUsage.findOneAndUpdate( { _id: userId }, [ @@ -93,7 +99,8 @@ export default class TokenUsageRateLimiter { */ async getCurrentUsage(userId) { const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec() - const featureUsage = reportedUsage?.features?.[this.featureName] ?? {} + const featureUsage = + /** @type {any} */ (reportedUsage)?.features?.[this.featureName] ?? {} return { usage: featureUsage.usage ?? 0, periodStart: featureUsage.periodStart ?? new Date(), @@ -112,7 +119,8 @@ export default class TokenUsageRateLimiter { async getRemainingTokens(userId) { const allowance = await this._getAllowance(userId) const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec() - const featureUsage = reportedUsage?.features?.[this.featureName] ?? {} + const featureUsage = + /** @type {any} */ (reportedUsage)?.features?.[this.featureName] ?? {} const periodStart = featureUsage.periodStart ?? new Date() const usage = featureUsage.usage ?? 0 const usesLeft = allowance - usage diff --git a/services/web/frontend/stylesheets/abstracts/mixins.scss b/services/web/frontend/stylesheets/abstracts/mixins.scss index 6f39fd32e3..91b9d2e81f 100644 --- a/services/web/frontend/stylesheets/abstracts/mixins.scss +++ b/services/web/frontend/stylesheets/abstracts/mixins.scss @@ -1,3 +1,5 @@ +@use 'sass:list'; + // Button @mixin ol-button-size( @@ -105,7 +107,7 @@ } @mixin theme($name) { - @if index($themes, $name) { + @if list.index($themes, $name) { [data-theme='#{$name}'] { @content; } diff --git a/services/web/package.json b/services/web/package.json index 7d79965eea..7905388d34 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -253,6 +253,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/algoliasearch": "^3.34.11", + "@types/async": "^3.2.25", "@types/bootstrap": "^5.2.10", "@types/chai": "^4.3.0", "@types/dateformat": "^5.0.3", @@ -260,6 +261,7 @@ "@types/dom-speech-recognition": "^0.0.7", "@types/events": "^3.0.3", "@types/express": "^4.17.23", + "@types/minimist": "^1.2.5", "@types/mocha": "^9.1.0", "@types/mocha-each": "^2.0.4", "@types/node": "^24.5.2", @@ -270,7 +272,9 @@ "@types/react-linkify": "^1.0.4", "@types/react-syntax-highlighter": "^15.5.11", "@types/recurly__recurly-js": "^4.38.0", + "@types/sanitize-html": "^2.14.0", "@types/sinon-chai": "^3.2.12", + "@types/utf-8-validate": "^5.0.2", "@types/uuid": "^9.0.8", "@uppy/core": "^3.8.0", "@uppy/dashboard": "^3.7.1", diff --git a/services/web/scripts/attach_dangling_comments_to_doc.mjs b/services/web/scripts/attach_dangling_comments_to_doc.mjs index 32edb44659..9b210bc330 100644 --- a/services/web/scripts/attach_dangling_comments_to_doc.mjs +++ b/services/web/scripts/attach_dangling_comments_to_doc.mjs @@ -65,6 +65,10 @@ async function getDanglingThreads(projectId) { return danglingThreads } +/** + * @param {any} projectId + * @param {any} docId + */ const ensureDocExists = async (projectId, docId) => { const doc = await DocstoreManager.promises.getDoc(projectId, docId) if (!doc) { diff --git a/services/web/scripts/backfill_mixpanel_user_properties.mjs b/services/web/scripts/backfill_mixpanel_user_properties.mjs index 455b10a5c7..053651fa1c 100644 --- a/services/web/scripts/backfill_mixpanel_user_properties.mjs +++ b/services/web/scripts/backfill_mixpanel_user_properties.mjs @@ -13,6 +13,9 @@ const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY || '10', 10) const mixpanelSinkQueue = getQueue('analytics-mixpanel-sink') +/** + * @param {any} user + */ async function processUser(user) { const analyticsId = user.analyticsId || user._id @@ -58,6 +61,9 @@ async function processUser(user) { } } +/** + * @param {any} userId + */ async function _getGroupSubscriptionPlanCode(userId) { const subscriptions = await SubscriptionLocator.promises.getMemberSubscriptions(userId) @@ -77,6 +83,12 @@ async function _getGroupSubscriptionPlanCode(userId) { return bestPlanCode } +/** + * @param {any} analyticsId + * @param {any} propertyName + * @param {any} propertyValue + * @param {any} [createdAt] + */ async function _sendPropertyToQueue( analyticsId, propertyName, @@ -94,6 +106,10 @@ async function _sendPropertyToQueue( }) } +/** + * @param {any} _ + * @param {any} users + */ async function processBatch(_, users) { await promiseMapWithLimit(WRITE_CONCURRENCY, users, async user => { await processUser(user) diff --git a/services/web/scripts/check_docs.mjs b/services/web/scripts/check_docs.mjs index a7efb71307..a877734a17 100644 --- a/services/web/scripts/check_docs.mjs +++ b/services/web/scripts/check_docs.mjs @@ -149,6 +149,9 @@ function getProjectIds() { .map(x => x._id.toString()) } +/** + * @param {any} projectId + */ async function getDocs(projectId) { const mongoDocs = db.docs.find( { @@ -192,6 +195,10 @@ async function getDocs(projectId) { return docs } +/** + * @param {any} projectId + * @param {any} docs + */ async function findDanglingThreadIds(projectId, docs) { const threadIds = new Set() for (const doc of docs) { @@ -219,6 +226,9 @@ async function findDanglingThreadIds(projectId, docs) { return Array.from(threadIds) } +/** + * @param {any} docs + */ function docsHaveTrackedChanges(docs) { for (const doc of docs) { const changes = doc.ranges?.changes ?? [] @@ -229,6 +239,9 @@ function docsHaveTrackedChanges(docs) { return false } +/** + * @param {any} docs + */ function docsHaveAnyComments(docs) { for (const doc of docs) { const comments = doc.ranges?.comments ?? [] diff --git a/services/web/scripts/convert_track_changes_to_explicit_format.mjs b/services/web/scripts/convert_track_changes_to_explicit_format.mjs index 226b5121c2..8f3f6fb7d5 100644 --- a/services/web/scripts/convert_track_changes_to_explicit_format.mjs +++ b/services/web/scripts/convert_track_changes_to_explicit_format.mjs @@ -48,6 +48,9 @@ async function main(trackProgress) { ) } +/** + * @param {any} project + */ async function processProject(project) { if (DEBUG) { console.log( diff --git a/services/web/scripts/delete_dangling_comments.mjs b/services/web/scripts/delete_dangling_comments.mjs index 7fcc56a6d2..f36aa2b834 100644 --- a/services/web/scripts/delete_dangling_comments.mjs +++ b/services/web/scripts/delete_dangling_comments.mjs @@ -29,6 +29,9 @@ function parseArgs() { } } +/** + * @param {any} projectId + */ async function processProject(projectId) { console.log(`Processing project ${projectId}...`) await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(projectId) @@ -49,6 +52,11 @@ async function processProject(projectId) { } } +/** + * @param {any} projectId + * @param {any} doc + * @param {any} threadIds + */ async function processDoc(projectId, doc, threadIds) { let commentsDeleted = 0 for (const comment of doc.ranges?.comments ?? []) { @@ -66,6 +74,10 @@ async function processDoc(projectId, doc, threadIds) { return commentsDeleted } +/** + * @param {any} docId + * @param {any} threadId + */ async function deleteComment(docId, threadId) { await db.docs.updateOne( { _id: new ObjectId(docId) }, diff --git a/services/web/scripts/fix_comment_id.mjs b/services/web/scripts/fix_comment_id.mjs index a073e33102..5fcc3d903f 100644 --- a/services/web/scripts/fix_comment_id.mjs +++ b/services/web/scripts/fix_comment_id.mjs @@ -24,6 +24,9 @@ function parseArgs() { } } +/** + * @param {any} projectId + */ async function processProject(projectId) { console.log(`Processing project ${projectId}...`) const docRanges = await DocstoreManager.promises.getAllRanges(projectId) @@ -38,6 +41,9 @@ async function processProject(projectId) { } } +/** + * @param {any} doc + */ async function processDoc(doc) { let commentsUpdated = 0 for (const comment of doc.ranges.comments ?? []) { diff --git a/services/web/scripts/fix_unconfirmed_secondaries_not_removed_v1.mjs b/services/web/scripts/fix_unconfirmed_secondaries_not_removed_v1.mjs index 51ba4598dc..76aaddd3d7 100644 --- a/services/web/scripts/fix_unconfirmed_secondaries_not_removed_v1.mjs +++ b/services/web/scripts/fix_unconfirmed_secondaries_not_removed_v1.mjs @@ -42,6 +42,9 @@ if (!doNotListUsers) { */ const parseAsync = promisify(csv.parse) +/** + * @param {any} userId + */ async function getV1Affiliations(userId) { const url = `${Settings.apis.v1.url}/api/v2/users/${userId}/affiliations` @@ -56,6 +59,10 @@ async function getV1Affiliations(userId) { return affiliations } +/** + * @param {any} userId + * @param {any} email + */ async function removeAffiliationV1(userId, email) { const url = `${Settings.apis.v1.url}/api/v2/users/${userId}/affiliations/remove` @@ -92,6 +99,9 @@ const results = { errorRemovingAffiliationInV1: [], } +/** + * @param {any} trackProgress + */ async function main(trackProgress) { console.time('check_removed_emails') @@ -99,6 +109,7 @@ async function main(trackProgress) { const csvContent = await fs.readFile(filePath, 'utf8') const rows = await parseAsync(csvContent) rows.shift() // Remove header row + /** @type {Record} */ const emailsByUserId = {} for (const [userId, email] of rows) { @@ -124,12 +135,12 @@ async function main(trackProgress) { // nothing to cleanup in v1 if no affiliations for the user continue } - } catch (e) { + } catch (/** @type {any} */ e) { results.errorCheckingAffiliations.push(userId) } const affiliationsEmailsInV1 = affiliations.map( - affiliation => affiliation.email + (/** @type {any} */ affiliation) => affiliation.email ) const user = await db.users.findOne( @@ -151,7 +162,9 @@ async function main(trackProgress) { continue } - const emailOnAccount = user?.emails?.find(e => e.email === email) + const emailOnAccount = user?.emails?.find( + (/** @type {any} */ e) => e.email === email + ) if (emailOnAccount) { // the email is still on the user account, we should not remove the affiliation in v1 @@ -181,7 +194,7 @@ async function main(trackProgress) { // remove the affiliation in v1 await removeAffiliationV1(userId, email) results.successfullyRemovedEmailInV1ForUser.push({ userId, email }) - } catch (e) { + } catch (/** @type {any} */ e) { results.errorRemovingAffiliationInV1.push({ userId, email, @@ -200,18 +213,18 @@ async function main(trackProgress) { console.log('Results:') for (const key in results) { - console.log(` ${key}:`, results[key].length) + console.log(` ${key}:`, /** @type {any} */ (results)[key].length) } for (const key in results) { if ( !doNotListUsers && - results[key].length > 0 && + /** @type {any} */ (results)[key].length > 0 && key !== 'needToRemoveEmailInV1' ) { // skip needToRemoveEmailInV1 since we'll only output that if this list length does not match success list length console.log('----------------------------') console.log(`${key}:`) - console.log(results[key]) + console.log(/** @type {any} */ (results)[key]) } } diff --git a/services/web/scripts/recurly/generate_addon_prices.mjs b/services/web/scripts/recurly/generate_addon_prices.mjs index 6ae9d1846d..d2f79bcb99 100644 --- a/services/web/scripts/recurly/generate_addon_prices.mjs +++ b/services/web/scripts/recurly/generate_addon_prices.mjs @@ -14,6 +14,7 @@ async function main() { process.exit(1) } + /** @type {Record} */ const localizedAddOnsPricing = {} const monthlyPlan = await getPlan(ADD_ON_CODE) @@ -22,10 +23,12 @@ async function main() { process.exit(1) } for (const { currency, unitAmount } of monthlyPlan.currencies ?? []) { - if (!localizedAddOnsPricing[currency]) { - localizedAddOnsPricing[currency] = { [ADD_ON_CODE]: {} } + /** @type {any} */ + const curr = currency + if (!localizedAddOnsPricing[curr]) { + localizedAddOnsPricing[curr] = { [ADD_ON_CODE]: {} } } - localizedAddOnsPricing[currency][ADD_ON_CODE].monthly = unitAmount + localizedAddOnsPricing[curr][ADD_ON_CODE].monthly = unitAmount } const annualPlan = await getPlan(`${ADD_ON_CODE}-annual`) @@ -34,11 +37,13 @@ async function main() { process.exit(1) } for (const { currency, unitAmount } of annualPlan.currencies ?? []) { - if (!localizedAddOnsPricing[currency]) { - localizedAddOnsPricing[currency] = { [ADD_ON_CODE]: {} } + /** @type {any} */ + const curr = currency + if (!localizedAddOnsPricing[curr]) { + localizedAddOnsPricing[curr] = { [ADD_ON_CODE]: {} } } - localizedAddOnsPricing[currency][ADD_ON_CODE].annual = unitAmount - localizedAddOnsPricing[currency][ADD_ON_CODE].annualDividedByTwelve = + localizedAddOnsPricing[curr][ADD_ON_CODE].annual = unitAmount + localizedAddOnsPricing[curr][ADD_ON_CODE].annualDividedByTwelve = (unitAmount || 0) / 12 } diff --git a/services/web/scripts/recurly/set_manually_collected_subscriptions.mjs b/services/web/scripts/recurly/set_manually_collected_subscriptions.mjs index 7c7ce9c9ff..b83a5993b2 100644 --- a/services/web/scripts/recurly/set_manually_collected_subscriptions.mjs +++ b/services/web/scripts/recurly/set_manually_collected_subscriptions.mjs @@ -73,6 +73,9 @@ function usage() { where FILE contains the list of subscription ids that are manually collected`) } +/** + * @param {any} filename + */ function readFile(filename) { const contents = fs.readFileSync(filename, { encoding: 'utf-8' }) const subscriptionIds = contents.split('\n').filter(id => id.length > 0) diff --git a/services/web/scripts/stripe/create_prices_from_csv.mjs b/services/web/scripts/stripe/create_prices_from_csv.mjs index bf73dd8763..9f37c92b29 100644 --- a/services/web/scripts/stripe/create_prices_from_csv.mjs +++ b/services/web/scripts/stripe/create_prices_from_csv.mjs @@ -62,6 +62,7 @@ async function getExistingPrices(stripe) { let startingAfter do { + /** @type {any} */ const response = await stripe.prices.list({ limit: 100, starting_after: startingAfter, @@ -89,6 +90,7 @@ async function getExistingProducts(stripe) { let startingAfter do { + /** @type {any} */ const response = await stripe.products.list({ limit: 100, starting_after: startingAfter, @@ -104,6 +106,9 @@ async function getExistingProducts(stripe) { return productsById } +/** + * @param {any} trackProgress + */ export async function main(trackProgress) { const args = minimist(process.argv.slice(2), { boolean: ['commit'], @@ -175,7 +180,10 @@ export async function main(trackProgress) { record.productName || planCode .split(/[_-]/) // Handle underscores or hyphens - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .map( + /** @param {any} word */ + word => word.charAt(0).toUpperCase() + word.slice(1) + ) .join(' ') if (commit) { @@ -203,7 +211,7 @@ export async function main(trackProgress) { // 2. Handle Prices for each currency column for (const currency of currencyKeys) { - const amountValue = parseFloat(record[currency]) + const amountValue = parseFloat(/** @type {any} */ (record)[currency]) if (isNaN(amountValue) || amountValue <= 0) continue const currencyLower = currency.toLowerCase() diff --git a/services/web/tsconfig.backend.json b/services/web/tsconfig.backend.json index fa54dc4bdf..6549536bc8 100644 --- a/services/web/tsconfig.backend.json +++ b/services/web/tsconfig.backend.json @@ -1,5 +1,8 @@ { "extends": "../../tsconfig.backend.json", + "compilerOptions": { + "noImplicitAny": true + }, "include": [ "app/src/**/*", "modules/*/app/src/**/*",