diff --git a/migrations/20230712042655-settings-split-backup-config-policy-limit.js b/migrations/20230712042655-settings-split-backup-config-policy-limit.js index 0a87a62c1..28a1779c2 100644 --- a/migrations/20230712042655-settings-split-backup-config-policy-limit.js +++ b/migrations/20230712042655-settings-split-backup-config-policy-limit.js @@ -1,6 +1,6 @@ 'use strict'; -const _ = require('underscore'); +const _ = require('../src/underscore.js'); exports.up = async function(db) { const result = await db.runSql('SELECT * FROM settings WHERE name=?', [ 'backup_config' ]); @@ -10,10 +10,10 @@ exports.up = async function(db) { // split policy, limits from backupConfig const backupPolicy = backupConfig.schedulePattern && backupConfig.retentionPolicy ? { schedule: backupConfig.schedulePattern, retention: backupConfig.retentionPolicy } : null; - const storageConfig = _.omit(backupConfig, 'copyConcurrency', 'syncConcurrency', 'memoryLimit', 'downloadConcurrency', - 'deleteConcurrency', 'uploadPartSize', 'schedulePattern', 'retentionPolicy', 'mountStatus'); - const limits = _.pick(backupConfig, 'copyConcurrency', 'syncConcurrency', 'memoryLimit', 'downloadConcurrency', - 'deleteConcurrency', 'uploadPartSize'); + const storageConfig = _.omit(backupConfig, ['copyConcurrency', 'syncConcurrency', 'memoryLimit', 'downloadConcurrency', + 'deleteConcurrency', 'uploadPartSize', 'schedulePattern', 'retentionPolicy', 'mountStatus']); + const limits = _.pick(backupConfig, ['copyConcurrency', 'syncConcurrency', 'memoryLimit', 'downloadConcurrency', + 'deleteConcurrency', 'uploadPartSize']); await db.runSql('START TRANSACTION'); await db.runSql('UPDATE settings SET value=?,name=? WHERE name=?', [ JSON.stringify(storageConfig), 'backup_storage', 'backup_config']); // rename diff --git a/package-lock.json b/package-lock.json index e117f4de7..c675a22fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "yellowtent", + "name": "cloudron", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "yellowtent", + "name": "cloudron", "version": "1.0.0", "dependencies": { "@aws-sdk/client-route-53": "^3.744.0", @@ -52,7 +52,6 @@ "tar-stream": "^3.1.7", "tldjs": "^2.3.1", "ua-parser-js": "^2.0.2", - "underscore": "^1.13.7", "uuid": "^11.0.5", "validator": "^13.12.0", "ws": "^8.18.0", @@ -8214,12 +8213,6 @@ "node": ">= 0.8" } }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 2192d8c21..1e2782baa 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "tar-stream": "^3.1.7", "tldjs": "^2.3.1", "ua-parser-js": "^2.0.2", - "underscore": "^1.13.7", "uuid": "^11.0.5", "validator": "^13.12.0", "ws": "^8.18.0", diff --git a/src/apppasswords.js b/src/apppasswords.js index 909f7cacd..1bc29ac57 100644 --- a/src/apppasswords.js +++ b/src/apppasswords.js @@ -16,7 +16,7 @@ const assert = require('assert'), hat = require('./hat.js'), safe = require('safetydance'), uuid = require('uuid'), - _ = require('underscore'); + _ = require('./underscore.js'); const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(','); @@ -30,7 +30,7 @@ function validateAppPasswordName(name) { } function removePrivateFields(appPassword) { - return _.pick(appPassword, 'id', 'name', 'userId', 'identifier', 'creationTime'); + return _.pick(appPassword, ['id', 'name', 'userId', 'identifier', 'creationTime']); } async function get(id) { diff --git a/src/apps.js b/src/apps.js index 4fb2eb24e..362eee45b 100644 --- a/src/apps.js +++ b/src/apps.js @@ -188,7 +188,7 @@ const appstore = require('./appstore.js'), util = require('util'), uuid = require('uuid'), volumes = require('./volumes.js'), - _ = require('underscore'); + _ = require('./underscore.js'); // NOTE: when adding fields here, update the clone and unarchive logic as well const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', @@ -593,19 +593,19 @@ function pickFields(app, accessLevel) { let result; if (accessLevel === exports.ACCESS_LEVEL_USER) { - result = _.pick(app, + result = _.pick(app, [ 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', 'subdomain', 'domain', 'fqdn', 'certificate', - 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'upstreamUri'); + 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'upstreamUri']); } else { // admin or operator - result = _.pick(app, + result = _.pick(app, [ 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'subdomain', 'domain', 'fqdn', 'certificate', 'crontab', 'upstreamUri', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuQuota', 'operators', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'label', 'notes', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'devices', 'env', 'enableAutomaticUpdate', 'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableTurn', 'enableRedis', 'checklist', - 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain'); + 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain']); } // remove private certificate key @@ -1484,7 +1484,7 @@ async function install(data, auditSource) { const taskId = await addTask(appId, app.installationState, task, auditSource); - const newApp = Object.assign({}, _.omit(app, 'icon'), { appStoreId, manifest, subdomain, domain, portBindings }); + const newApp = Object.assign({}, _.omit(app, ['icon']), { appStoreId, manifest, subdomain, domain, portBindings }); newApp.fqdn = dns.fqdn(newApp.subdomain, newApp.domain); newApp.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); }); newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); }); @@ -2014,7 +2014,7 @@ async function setLocation(app, data, auditSource) { const task = { args: { - oldConfig: _.pick(app, 'subdomain', 'domain', 'fqdn', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'portBindings'), + oldConfig: _.pick(app, ['subdomain', 'domain', 'fqdn', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'portBindings']), skipDnsSetup: !!data.skipDnsSetup, overwriteDns: !!data.overwriteDns }, @@ -2436,10 +2436,10 @@ async function clone(app, data, user, auditSource) { const newAppId = uuid.v4(); // label, checklist intentionally omitted . icon is loaded in apptask from the backup - const dolly = _.pick(backupInfo.appConfig || app, 'memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices', + const dolly = _.pick(backupInfo.appConfig || app, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'debugMode', 'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso', - 'notes', 'checklist'); + 'notes', 'checklist']); if (!manifest.addons?.recvmail) dolly.inboxDomain = null; // needed because we are cloning _current_ app settings with old manifest @@ -2514,10 +2514,10 @@ async function unarchive(archive, data, auditSource) { const appId = uuid.v4(); // appConfig is null for pre-8.2 backups - const dolly = _.pick(backup.appConfig || {}, 'memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', + const dolly = _.pick(backup.appConfig || {}, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'label', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'devices', 'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso', - 'notes', 'checklist'); + 'notes', 'checklist']); // intentionally not filled up: redirectDomain, aliasDomains, mailboxDomain const obj = Object.assign(dolly, { @@ -3099,7 +3099,7 @@ async function loadConfig(app) { const appConfig = safe.JSON.parse(safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'))); let data = {}; if (appConfig) { - data = _.pick(appConfig, 'memoryLimit', 'cpuQuota', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate'); + data = _.pick(appConfig, ['memoryLimit', 'cpuQuota', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate']); } const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png')); diff --git a/src/apptask.js b/src/apptask.js index 297b58930..c2e3b8996 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -36,7 +36,7 @@ const apps = require('./apps.js'), safe = require('safetydance'), services = require('./services.js'), shell = require('./shell.js')('apptask'), - _ = require('underscore'); + _ = require('./underscore.js'); const MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'), LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }), @@ -345,7 +345,7 @@ async function install(app, args, progressCallback) { } else if (app.installationState === apps.ISTATE_PENDING_IMPORT && !restoreConfig.remotePath) { // in-place import await progressCallback({ percent: 60, message: 'Importing addons in-place' }); await services.setupAddons(app, app.manifest.addons); - await services.clearAddons(app, _.omit(app.manifest.addons, 'localstorage')); + await services.clearAddons(app, _.omit(app.manifest.addons, ['localstorage'])); await apps.loadConfig(app); await services.restoreAddons(app, app.manifest.addons); } else if (app.installationState === apps.ISTATE_PENDING_IMPORT) { // import diff --git a/src/backups.js b/src/backups.js index ce71f6c4d..9e5d4e973 100644 --- a/src/backups.js +++ b/src/backups.js @@ -71,7 +71,7 @@ const assert = require('assert'), settings = require('./settings.js'), storage = require('./storage.js'), tasks = require('./tasks.js'), - _ = require('underscore'); + _ = require('./underscore.js'); const BACKUPS_FIELDS = [ 'id', 'remotePath', 'label', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOnJson', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion', 'appConfigJson' ]; @@ -443,7 +443,7 @@ async function getConfig() { async function setConfig(backupConfig) { assert.strictEqual(typeof backupConfig, 'object'); - await settings.setJson(settings.BACKUP_STORAGE_KEY, _.omit(backupConfig, 'limits')); + await settings.setJson(settings.BACKUP_STORAGE_KEY, _.omit(backupConfig, ['limits'])); await settings.setJson(settings.BACKUP_LIMITS_KEY, backupConfig.limits || null); } diff --git a/src/cron.js b/src/cron.js index 4be4c99c0..9fd50b7cd 100644 --- a/src/cron.js +++ b/src/cron.js @@ -42,7 +42,7 @@ const appHealthMonitor = require('./apphealthmonitor.js'), system = require('./system.js'), updater = require('./updater.js'), updateChecker = require('./updatechecker.js'), - _ = require('underscore'); + _ = require('./underscore.js'); const gJobs = { autoUpdater: null, @@ -231,7 +231,7 @@ async function handleAutoupdatePatternChanged(pattern) { // fall through to update apps if box update never started (failed ubuntu or avx check) } - const appUpdateInfo = _.omit(updateInfo, 'box'); + const appUpdateInfo = _.omit(updateInfo, ['box']); if (Object.keys(appUpdateInfo).length > 0) { debug('Starting app autoupdate: %j', Object.keys(appUpdateInfo)); const [error] = await safe(apps.autoupdateApps(appUpdateInfo, AuditSource.CRON)); diff --git a/src/dns/cloudflare.js b/src/dns/cloudflare.js index 1c40127c8..ab41b4a44 100644 --- a/src/dns/cloudflare.js +++ b/src/dns/cloudflare.js @@ -20,7 +20,7 @@ const assert = require('assert'), safe = require('safetydance'), superagent = require('superagent'), waitForDns = require('./waitfordns.js'), - _ = require('underscore'); + _ = require('../underscore.js'); // we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints const CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4'; diff --git a/src/dns/gcdns.js b/src/dns/gcdns.js index 81768e011..fbb12f903 100644 --- a/src/dns/gcdns.js +++ b/src/dns/gcdns.js @@ -20,7 +20,7 @@ const assert = require('assert'), dns = require('../dns.js'), GCDNS = require('@google-cloud/dns').DNS, waitForDns = require('./waitfordns.js'), - _ = require('underscore'); + _ = require('../underscore.js'); function removePrivateFields(domainObject) { domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER; @@ -176,8 +176,8 @@ async function verifyDomainConfig(domainObject) { const zone = await getZoneByName(credentials, zoneName); - const definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); }); - if (!_.isEqual(definedNS, nameservers.sort())) { + const definedNS = zone.metadata.nameServers.map(function(r) { return r.replace(/\.$/, ''); }); + if (!_.isEqual(definedNS.sort(), nameservers.sort())) { debug('verifyDomainConfig: %j and %j do not match', nameservers, definedNS); throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'); } diff --git a/src/dns/route53.js b/src/dns/route53.js index ce0a1b133..62c7f0216 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -20,7 +20,7 @@ const assert = require('assert'), { Route53 } = require('@aws-sdk/client-route-53'), safe = require('safetydance'), waitForDns = require('./waitfordns.js'), - _ = require('underscore'); + _ = require('../underscore.js'); function removePrivateFields(domainObject) { domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER; diff --git a/src/dns/waitfordns.js b/src/dns/waitfordns.js index c1a3bcb69..062b5dd9f 100644 --- a/src/dns/waitfordns.js +++ b/src/dns/waitfordns.js @@ -9,7 +9,7 @@ const assert = require('assert'), dns = require('node:dns'), promiseRetry = require('../promise-retry.js'), safe = require('safetydance'), - _ = require('underscore'); + _ = require('../underscore.js'); async function resolveIp(hostname, type, options) { assert.strictEqual(typeof hostname, 'string'); @@ -28,7 +28,7 @@ async function resolveIp(hostname, type, options) { // recurse lookup the CNAME record debug(`resolveIp: found CNAME for ${hostname}. resolving ${cnameResults[0]}`); - return await dig.resolve(cnameResults[0], type, _.omit(options, 'server')); + return await dig.resolve(cnameResults[0], type, _.omit(options, ['server'])); } async function isChangeSynced(hostname, type, value, nameserver) { diff --git a/src/domains.js b/src/domains.js index 08bee1521..a860345ab 100644 --- a/src/domains.js +++ b/src/domains.js @@ -27,7 +27,7 @@ const assert = require('assert'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), tld = require('tldjs'), - _ = require('underscore'); + _ = require('./underscore.js'); const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(','); @@ -311,7 +311,7 @@ async function clear() { // removes all fields that are strictly private and should never be returned by API calls function removePrivateFields(domain) { - const result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'wellKnown'); + const result = _.pick(domain, ['domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'wellKnown']); const out = api(result.provider).removePrivateFields(result); delete out.fallbackCertificate.key; return out; diff --git a/src/mail.js b/src/mail.js index 0e7fe68d5..7ee31c6fd 100644 --- a/src/mail.js +++ b/src/mail.js @@ -80,7 +80,7 @@ const assert = require('assert'), shell = require('./shell.js')('mail'), superagent = require('superagent'), validator = require('validator'), - _ = require('underscore'); + _ = require('./underscore.js'); const DNS_OPTIONS = { timeout: 10000 }; const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh'); @@ -788,7 +788,7 @@ async function clearDomains() { // remove all fields that should never be sent out via REST API function removePrivateFields(domain) { - const result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay', 'banner'); + const result = _.pick(domain, ['domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay', 'banner']); if (result.relay.provider !== 'cloudron-smtp') { if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER; result.relay.password = constants.SECRET_PLACEHOLDER; diff --git a/src/platform.js b/src/platform.js index 2efb3d6ff..4004c57fb 100644 --- a/src/platform.js +++ b/src/platform.js @@ -37,7 +37,7 @@ const apps = require('./apps.js'), updater = require('./updater.js'), users = require('./users.js'), volumes = require('./volumes.js'), - _ = require('underscore'); + _ = require('./underscore.js'); let gStatusMessage = 'Initializing'; diff --git a/src/routes/appstore.js b/src/routes/appstore.js index ad2059556..13d5b59d8 100644 --- a/src/routes/appstore.js +++ b/src/routes/appstore.js @@ -17,7 +17,7 @@ const appstore = require('../appstore.js'), HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), users = require('../users.js'), - _ = require('underscore'); + _ = require('../underscore.js'); async function getApps(req, res, next) { const [error, apps] = await safe(appstore.getApps()); @@ -75,6 +75,6 @@ async function getSubscription(req, res, next) { if (error) return next(BoxError.toHttpError(error)); // non-owners only get a stripped down version - if (users.compareRoles(req.user.role, users.ROLE_OWNER) < 0) next(new HttpSuccess(200, _.pick(result, 'plan', 'status'))); + if (users.compareRoles(req.user.role, users.ROLE_OWNER) < 0) next(new HttpSuccess(200, _.pick(result, ['plan', 'status']))); else next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features } } diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index 141986d9d..333c70f86 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -9,7 +9,7 @@ const common = require('./common.js'), expect = require('expect.js'), mail = require('../../mail.js'), superagent = require('superagent'), - _ = require('underscore'); + _ = require('../../underscore.js'); describe('Mail API', function () { const { setup, cleanup, serverUrl, owner, dashboardDomain, dashboardFqdn, mailFqdn } = common; @@ -421,7 +421,7 @@ describe('Mail API', function () { .query({ access_token: owner.token }); expect(response.statusCode).to.equal(200); - expect(_.omit(response.body.relay, 'password')).to.eql(_.omit(relay, 'password')); + expect(_.omit(response.body.relay, ['password'])).to.eql(_.omit(relay, ['password'])); }); }); diff --git a/src/routes/users.js b/src/routes/users.js index aaeb6e03f..d89279789 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -34,7 +34,7 @@ const assert = require('assert'), HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), users = require('../users.js'), - _ = require('underscore'); + _ = require('../underscore.js'); async function load(req, res, next) { assert.strictEqual(typeof req.params.userId, 'string'); @@ -117,7 +117,7 @@ async function updateProfile(req, res, next) { if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but you are only '${req.user.role}'`)); - const data = _.pick(req.body, 'username', 'email', 'fallbackEmail', 'displayName'); + const data = _.pick(req.body, ['username', 'email', 'fallbackEmail', 'displayName']); const [error] = await safe(users.updateProfile(req.resource, data, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); diff --git a/src/scheduler.js b/src/scheduler.js index 0f3f47569..34854fb4f 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -17,7 +17,7 @@ const apps = require('./apps.js'), debug = require('debug')('box:scheduler'), docker = require('./docker.js'), safe = require('safetydance'), - _ = require('underscore'); + _ = require('./underscore.js'); const gState = {}; // appId -> { containerId, schedulerConfig (manifest+crontab), cronjobs } const gSuspendedAppIds = new Set(); // suspended because some apptask is running diff --git a/src/tasks.js b/src/tasks.js index 5cacf61b9..25681077c 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -54,7 +54,7 @@ const assert = require('assert'), paths = require('./paths.js'), safe = require('safetydance'), shell = require('./shell.js')('tasks'), - _ = require('underscore'); + _ = require('./underscore.js'); let gTasks = {}; // indexed by task id @@ -86,7 +86,7 @@ function updateStatus(result) { // running means actively running // pending means not actively running - // active mean task is 'done' or not. at this point, clients can stop polling this task. + // active mean task is 'done' or not. at this point, clients can stop polling this task. // the apptaskmanager sets percent=1 when queued. just a hack to figure non-started but scheduled tasks result.running = !!gTasks[result.id]; result.active = result.running || result.percent === 1; @@ -288,7 +288,7 @@ async function getLogs(task, options) { // removes all fields that are strictly private and should never be returned by API calls function removePrivateFields(task) { - return _.pick(task, 'id', 'type', 'percent', 'message', 'error', 'active', 'pending', 'creationTime', 'result', 'ts', 'success'); + return _.pick(task, ['id', 'type', 'percent', 'message', 'error', 'active', 'pending', 'creationTime', 'result', 'ts', 'success']); } async function del(id) { diff --git a/src/test/dns-providers-test.js b/src/test/dns-providers-test.js index 15e02a4af..d15a351b8 100644 --- a/src/test/dns-providers-test.js +++ b/src/test/dns-providers-test.js @@ -13,7 +13,7 @@ const common = require('./common.js'), expect = require('expect.js'), GCDNS = require('@google-cloud/dns').DNS, nock = require('nock'), - _ = require('underscore'); + _ = require('../underscore.js'); describe('dns provider', function () { const { setup, cleanup, auditSource, domain } = common; diff --git a/src/test/tasks-test.js b/src/test/tasks-test.js index edaeab6fd..38a234469 100644 --- a/src/test/tasks-test.js +++ b/src/test/tasks-test.js @@ -13,7 +13,7 @@ const BoxError = require('../boxerror.js'), paths = require('../paths.js'), safe = require('safetydance'), tasks = require('../tasks.js'), - _ = require('underscore'); + _ = require('../underscore.js'); describe('task', function () { const { setup, cleanup } = common; @@ -23,7 +23,7 @@ describe('task', function () { let taskId; - let TASK = { + const TASK = { type: 'tasktype', args: [ 1 ], percent: 0, diff --git a/src/test/users-test.js b/src/test/users-test.js index d7b40e09b..7447f3c60 100644 --- a/src/test/users-test.js +++ b/src/test/users-test.js @@ -11,7 +11,7 @@ const BoxError = require('../boxerror.js'), safe = require('safetydance'), speakeasy = require('speakeasy'), users = require('../users.js'), - _ = require('underscore'); + _ = require('../underscore.js'); describe('User', function () { const { domainSetup, cleanup, admin, user, auditSource, checkMails, clearMailQueue } = common; diff --git a/src/underscore.js b/src/underscore.js new file mode 100644 index 000000000..e857ae378 --- /dev/null +++ b/src/underscore.js @@ -0,0 +1,54 @@ +'use strict'; + +exports = module.exports = { + pick, + omit, + intersection, + isEqual, + difference +}; + +const assert = require('assert'); + +function pick(obj, keys) { + assert(Array.isArray(keys)); + + return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key))); +} + +function omit(obj, keys) { + assert(Array.isArray(keys)); + + return Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key))); +} + +function intersection(arr1, arr2) { + assert(Array.isArray(arr1)); + assert(Array.isArray(arr2)); + + return arr1.filter(value => arr2.includes(value)); +} + +// can be arrays or objects +function isEqual(val1, val2) { + if (val1 === val2) return true; + + if (Array.isArray(val1) && Array.isArray(val2)) { + if (val1.length !== val2.length) return false; + return val1.every((item, index) => isEqual(item, val2[index])); + } else if (typeof val1 === 'object' && typeof val2 === 'object') { + const keys1 = Object.keys(val1), keys2 = Object.keys(val2); + if (keys1.length !== keys2.length) return false; + return keys1.every(key => isEqual(val1[key], val2[key])); // Recursively compare object properties + } else { + return false; + } +} + +function difference(array, values) { + assert(Array.isArray(array)); + assert(Array.isArray(values)); + + const valueSet = new Set(values); + return array.filter(item => !valueSet.has(item)); +} diff --git a/src/users.js b/src/users.js index 1eef61086..c0514e886 100644 --- a/src/users.js +++ b/src/users.js @@ -103,7 +103,7 @@ const appPasswords = require('./apppasswords.js'), superagent = require('superagent'), util = require('util'), validator = require('validator'), - _ = require('underscore'); + _ = require('./underscore.js'); const CRYPTO_SALT_SIZE = 64; // 512-bit salt const CRYPTO_ITERATIONS = 10000; // iterations @@ -191,7 +191,9 @@ function validatePassword(password) { // remove all fields that should never be sent out via REST API function removePrivateFields(user) { - const result = _.pick(user, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'active', 'source', 'role', 'createdAt', 'twoFactorAuthenticationEnabled', 'notificationConfig'); + const result = _.pick(user, [ + 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'active', 'source', 'role', 'createdAt', + 'twoFactorAuthenticationEnabled', 'notificationConfig']); // invite status indicator result.inviteAccepted = !user.inviteToken; @@ -596,8 +598,6 @@ async function update(user, data, auditSource) { if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode'); - if (_.isEmpty(data)) return; - if (data.username) { // regardless of "account setup", username cannot be changed because admin could have logged in with temp password and apps // already know about it