mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Enable TS noImplicitAny in web (#31636)
GitOrigin-RevId: 18881694770f2476c475f8fef4c6a2678a2a12fe
This commit is contained in:
committed by
Copybot
parent
b3489a6792
commit
6113c6c291
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ mongo-utils
|
||||
--node-version=24.13.0
|
||||
--pipeline-owner=32
|
||||
--public-repo=False
|
||||
--tsconfig-no-implicit-any=True
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ object-persistor
|
||||
--node-version=24.13.0
|
||||
--pipeline-owner=32
|
||||
--public-repo=False
|
||||
--tsconfig-no-implicit-any=True
|
||||
|
||||
@@ -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<Array<RootKeyEncryptionKey>>} getRootKeyEncryptionKeys
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} S3PersistorSettings
|
||||
* @property {Object<string, import('@aws-sdk/client-s3').StorageClass>} [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<string, {auth_key: string, auth_secret: string}>} [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<void>}
|
||||
*/
|
||||
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<NodeJS.ReadableStream>}
|
||||
*/
|
||||
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<number>}
|
||||
*/
|
||||
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<string | undefined>}
|
||||
*/
|
||||
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<number>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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<string>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
async copyObject(bucketName, sourcePath, destinationPath, opts = {}) {
|
||||
const ssecOptions =
|
||||
opts.ssecOptions ||
|
||||
|
||||
@@ -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<string, StorageClass>} [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<string, {auth_key: string, auth_secret: string}>} [bucketCreds]
|
||||
*/
|
||||
|
||||
class S3Persistor extends AbstractPersistor {
|
||||
/** @type {Map<string, S3Client>} */
|
||||
#clients = new Map()
|
||||
/** @type {S3PersistorSettings} */
|
||||
settings
|
||||
|
||||
/**
|
||||
* @param {S3PersistorSettings} [settings]
|
||||
*/
|
||||
constructor(settings = {}) {
|
||||
super()
|
||||
|
||||
settings.storageClass = settings.storageClass || {}
|
||||
settings.storageClass = /** @type {Object<string, StorageClass>} */ (
|
||||
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<void>}
|
||||
*/
|
||||
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<string, StorageClass>} */ (
|
||||
this.settings.storageClass
|
||||
)
|
||||
if (storageClass[bucketName]) {
|
||||
uploadOptions.StorageClass = storageClass[bucketName]
|
||||
}
|
||||
|
||||
if ('sourceMd5' in opts) {
|
||||
@@ -239,7 +276,9 @@ class S3Persistor extends AbstractPersistor {
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ const rateLimiters = {
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} webRouter
|
||||
* @param {any} privateApiRouter
|
||||
*/
|
||||
function apply(webRouter, privateApiRouter) {
|
||||
// Blobs
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ function oldDebugProjects(userId) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
const NotificationsBuilder = {
|
||||
// Note: notification keys should be url-safe
|
||||
dropboxUnlinkedDueToLapsedReconfirmation(userId) {
|
||||
@@ -291,6 +292,7 @@ const NotificationsBuilder = {
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
NotificationsBuilder.promises = {
|
||||
dropboxUnlinkedDueToLapsedReconfirmation,
|
||||
redundantPersonalSubscription,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<string, any>} 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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, any>} */
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>} subscription - The subscription object
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
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<string, any>} subscription - The subscription object
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
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<string, any>} user - The user object
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
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<string, any>} 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<void>}
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<Object>} - The updated subscription document with recomputed state
|
||||
* @param {any} subscription - The MongoDB subscription document
|
||||
* @returns {Promise<any>} - 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ArrayBufferLike>} bytes
|
||||
*/
|
||||
function _detectEncoding(bytes) {
|
||||
if (isUtf8(bytes)) {
|
||||
return 'utf-8'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>} */ (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<string, any>} */ (
|
||||
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()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
|
||||
/**
|
||||
* @typedef {Object} RequestContext
|
||||
* @property {Object.<string, array>} [userFullEmails] - Dictionary mapping userId to an array of full emails
|
||||
* @typedef {Record<string, any>} RequestContext
|
||||
* @property {Record<string, any[]>} [userFullEmails] - Dictionary mapping userId to an array of full emails
|
||||
*/
|
||||
|
||||
/** @type {AsyncLocalStorage<RequestContext>} */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ const MODULE_BASE_PATH = Path.join(import.meta.dirname, '/../../../modules')
|
||||
/** @type {WebModule[]} */
|
||||
const _modules = []
|
||||
let _modulesLoaded = false
|
||||
/** @type {Record<string, any>} */
|
||||
const _hooks = {}
|
||||
|
||||
/** @type {Record<string, RequestHandler[]>} */
|
||||
const _middleware = {}
|
||||
/** @type {Record<string, any>} */
|
||||
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<string, any>} */
|
||||
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<string, any>} */
|
||||
const agents = {}
|
||||
for (const module of await modules()) {
|
||||
for (const name in module.linkedFileAgents) {
|
||||
const agentFunction = module.linkedFileAgents[name]
|
||||
const agentFunction = /** @type {Record<string, any>} */ (
|
||||
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<string, any>} */ (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<string, any>} */ (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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -88,7 +88,12 @@ export default class FeatureUsageRateLimiter {
|
||||
upsert: true,
|
||||
}
|
||||
).exec()
|
||||
const featureUsage = featureUsages.features?.[this.featureName] ?? {}
|
||||
|
||||
const featureUsage =
|
||||
/** @type {Record<string, any>} */ (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<string, any>} */ (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<string, any>} */ (reportedUsage?.features ?? {})[
|
||||
this.featureName
|
||||
] ?? {}
|
||||
const periodStart = featureUsage.periodStart ?? new Date()
|
||||
const usage = featureUsage.usage ?? 0
|
||||
const usesLeft = allowance - usage
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ?? []
|
||||
|
||||
@@ -48,6 +48,9 @@ async function main(trackProgress) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} project
|
||||
*/
|
||||
async function processProject(project) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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 ?? []) {
|
||||
|
||||
@@ -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<string, string[]>} */
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ async function main() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/** @type {Record<string, any>} */
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": [
|
||||
"app/src/**/*",
|
||||
"modules/*/app/src/**/*",
|
||||
|
||||
Reference in New Issue
Block a user