Revert "Add no-use-before-define linter rule"

This reverts commit fdcc5d68a2.

Unfortunately, this requires us to move exports to the bottom.
This in turn causes circular dep issues and also access of
exports.GLOBAL_VAR in the global context
This commit is contained in:
Girish Ramakrishnan
2025-10-08 20:11:55 +02:00
parent a5224258c3
commit 43e426ab9f
41 changed files with 718 additions and 681 deletions

View File

@@ -14,8 +14,7 @@ module.exports = [
},
rules: {
semi: "error",
"prefer-const": "error",
"no-use-before-define": "error",
"prefer-const": "error"
}
}
];

View File

@@ -1,5 +1,9 @@
'use strict';
exports = module.exports = {
run
};
const apps = require('./apps.js'),
assert = require('node:assert'),
AuditSource = require('./auditsource.js'),
@@ -11,10 +15,6 @@ const apps = require('./apps.js'),
safe = require('safetydance'),
superagent = require('@cloudron/superagent');
exports = module.exports = {
run
};
const UNHEALTHY_THRESHOLD = 20 * 60 * 1000; // 20 minutes
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // will only raise 1 oom event every hour

View File

@@ -1,5 +1,16 @@
'use strict';
exports = module.exports = {
download,
upload,
verify,
getFileExtension,
copy,
_saveFsMetadata: saveFsMetadata,
_restoreFsMetadata: restoreFsMetadata
};
const assert = require('node:assert'),
async = require('async'),
backupSites = require('../backupsites.js'),
@@ -372,13 +383,3 @@ async function verify(backupSite, remotePath, integrityMap, progressCallback) {
};
}
exports = module.exports = {
download,
upload,
verify,
getFileExtension,
copy,
_saveFsMetadata: saveFsMetadata,
_restoreFsMetadata: restoreFsMetadata
};

View File

@@ -1,5 +1,9 @@
'use strict';
exports = module.exports = {
check
};
const backups = require('./backups.js'),
backupFormats = require('./backupformats.js'),
backupSites = require('./backupsites.js'),
@@ -32,6 +36,3 @@ async function check(backupId, progressCallback) {
};
}
exports = module.exports = {
check
};

View File

@@ -1,5 +1,33 @@
'use strict';
exports = module.exports = {
get,
getByIdentifierAndStatePaged,
getLatestInTargetByIdentifier, // brutal function name
add,
update,
setState,
list,
listBySiteId,
listByTypePaged,
del,
removePrivateFields,
startIntegrityCheck,
BACKUP_IDENTIFIER_BOX: 'box',
BACKUP_IDENTIFIER_MAIL: 'mail',
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_TYPE_MAIL: 'mail',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
BACKUP_STATE_CREATING: 'creating',
BACKUP_STATE_ERROR: 'error',
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
@@ -195,30 +223,3 @@ async function startIntegrityCheck(backup) {
return taskId;
}
exports = module.exports = {
get,
getByIdentifierAndStatePaged,
getLatestInTargetByIdentifier, // brutal function name
add,
update,
setState,
list,
listBySiteId,
listByTypePaged,
del,
removePrivateFields,
startIntegrityCheck,
BACKUP_IDENTIFIER_BOX: 'box',
BACKUP_IDENTIFIER_MAIL: 'mail',
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_TYPE_MAIL: 'mail',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
BACKUP_STATE_CREATING: 'creating',
BACKUP_STATE_ERROR: 'error',
};

View File

@@ -1,5 +1,19 @@
'use strict';
exports = module.exports = {
fullBackup,
appBackup,
restore,
downloadApp,
backupApp,
downloadMail,
upload,
};
const apps = require('./apps.js'),
assert = require('node:assert'),
backupFormats = require('./backupformats.js'),
@@ -557,16 +571,3 @@ async function appBackup(appId, backupSiteId, options, progressCallback) {
return backupId;
}
exports = module.exports = {
fullBackup,
appBackup,
restore,
downloadApp,
backupApp,
downloadMail,
upload,
};

View File

@@ -1,13 +1,13 @@
'use strict';
const assert = require('node:assert'),
fs = require('node:fs'),
path = require('node:path');
exports = module.exports = {
getChanges
};
const assert = require('node:assert'),
fs = require('node:fs'),
path = require('node:path');
function getChanges(version) {
assert.strictEqual(typeof version, 'string');

View File

@@ -1,6 +1,6 @@
'use strict';
module.exports = exports = {
exports = module.exports = {
fqdn,
getName,

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -246,12 +256,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -282,12 +292,3 @@ async function verifyDomainConfig(domainObject) {
return sanitizedConfig;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
@@ -157,12 +167,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -243,12 +253,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -255,12 +265,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -162,12 +172,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -183,12 +193,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -183,12 +193,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -249,12 +259,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const { ApiClient, Language } = require('domrobot-client'),
assert = require('node:assert'),
BoxError = require('../boxerror.js'),
@@ -208,12 +218,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
@@ -258,12 +268,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
verifyDomainConfig,
wait
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -275,12 +285,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
verifyDomainConfig,
wait
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -241,12 +251,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -253,12 +263,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -225,12 +235,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -217,12 +227,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig,
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
{ ConfiguredRetryStrategy } = require('@smithy/util-retry'),
@@ -261,12 +271,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig,
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};
const assert = require('node:assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
@@ -228,12 +238,3 @@ async function verifyDomainConfig(domainObject) {
return credentials;
}
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDomainConfig
};

View File

@@ -1,5 +1,15 @@
'use strict';
exports = module.exports = {
removePrivateFields,
list,
add,
get,
del,
update,
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
@@ -111,12 +121,3 @@ async function del(registry, auditSource) {
await eventlog.add(eventlog.ACTION_REGISTRY_DEL, auditSource, { registry: removePrivateFields(registry) });
}
exports = module.exports = {
removePrivateFields,
list,
add,
get,
del,
update,
};

View File

@@ -1,6 +1,6 @@
'use strict';
module.exports = exports = {
exports = module.exports = {
add,
get,
list,

View File

@@ -1,5 +1,21 @@
'use strict';
exports = module.exports = {
getConfig,
setConfig,
verifyPassword,
maybeCreateUser,
supports2FA,
startSyncer,
removePrivateFields,
sync
};
const assert = require('node:assert'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
@@ -523,18 +539,3 @@ async function sync(progressCallback) {
debug('sync: done');
}
exports = module.exports = {
getConfig,
setConfig,
verifyPassword,
maybeCreateUser,
supports2FA,
startSyncer,
removePrivateFields,
sync
};

View File

@@ -1,5 +1,10 @@
'use strict';
exports = module.exports = {
cleanupTokens,
cleanupDockerVolumes
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:janitor'),
@@ -7,11 +12,6 @@ const assert = require('node:assert'),
safe = require('safetydance'),
tokens = require('./tokens.js');
exports = module.exports = {
cleanupTokens,
cleanupDockerVolumes
};
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
async function cleanupTokens() {

View File

@@ -1,5 +1,65 @@
'use strict';
exports = module.exports = {
getStatus,
checkConfiguration,
listDomains,
getDomain,
clearDomains,
removePrivateFields,
setDnsRecords,
upsertDnsRecords,
validateName,
validateDisplayName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
sendTestMail,
getMailboxCount,
listMailboxes,
listAllMailboxes,
getMailbox,
addMailbox,
updateMailbox,
delMailbox,
getAlias,
getAliases,
setAliases,
searchAlias,
getListCount,
getLists,
getList,
addList,
updateList,
delList,
resolveList,
checkStatus,
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
OWNERTYPE_APP: 'app',
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
_delByDomain: delByDomain,
_updateDomain: updateDomain
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
@@ -27,8 +87,6 @@ const assert = require('node:assert'),
const DNS_OPTIONS = { timeout: 20000, tries: 4 };
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ];
// if you add a field here, listMailboxes has to be updated
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3', 'storageQuota', 'messagesQuota' ].join(',');
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
@@ -111,6 +169,11 @@ function validateDisplayName(name) {
return null;
}
function validateOwnerType(type) {
const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ];
return OWNERTYPES.includes(type);
}
async function getDomain(domain) {
assert.strictEqual(typeof domain, 'string');
@@ -862,7 +925,7 @@ async function addMailbox(name, domain, data, auditSource) {
let error = validateName(name);
if (error) throw error;
if (!OWNERTYPES.includes(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
if (!validateOwnerType(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3 ]));
@@ -888,7 +951,7 @@ async function updateMailbox(name, domain, data, auditSource) {
continue;
}
if (k === 'ownerType' && !OWNERTYPES.includes(data[k])) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
if (k === 'ownerType' && !validateOwnerType(data[k])) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
fields.push(k + ' = ?');
args.push(data[k]);
@@ -1177,62 +1240,3 @@ async function checkStatus() {
}
}
exports = module.exports = {
getStatus,
checkConfiguration,
listDomains,
getDomain,
clearDomains,
removePrivateFields,
setDnsRecords,
upsertDnsRecords,
validateName,
validateDisplayName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
sendTestMail,
getMailboxCount,
listMailboxes,
listAllMailboxes,
getMailbox,
addMailbox,
updateMailbox,
delMailbox,
getAlias,
getAliases,
setAliases,
searchAlias,
getListCount,
getLists,
getList,
addList,
updateList,
delList,
resolveList,
checkStatus,
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
OWNERTYPE_APP: 'app',
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
_delByDomain: delByDomain,
_updateDomain: updateDomain
};

View File

@@ -200,6 +200,13 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
await shell.bash(runCmd, { encoding: 'utf8' });
}
async function getLocation() {
const subdomain = await settings.get(settings.MAIL_SUBDOMAIN_KEY);
const domain = await settings.get(settings.MAIL_DOMAIN_KEY);
return new Location(subdomain, domain, Location.TYPE_MAIL);
}
async function restart() {
if (constants.TEST && !process.env.TEST_CREATE_INFRA) return;
@@ -265,13 +272,6 @@ async function checkCertificate() {
await restartIfActivated();
}
async function getLocation() {
const subdomain = await settings.get(settings.MAIL_SUBDOMAIN_KEY);
const domain = await settings.get(settings.MAIL_DOMAIN_KEY);
return new Location(subdomain, domain, Location.TYPE_MAIL);
}
async function changeLocation(auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
@@ -351,3 +351,4 @@ async function getMailAuth() {
relayToken
};
}

View File

@@ -1,5 +1,16 @@
'use strict';
exports = module.exports = {
add,
get,
update,
del,
list,
load,
remount,
getStatus
};
const assert = require('node:assert'),
AuditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
@@ -84,13 +95,3 @@ async function getStatus(req, res, next) {
next(new HttpSuccess(200, status));
}
exports = module.exports = {
add,
get,
update,
del,
list,
load,
remount,
getStatus
};

View File

@@ -1,5 +1,7 @@
'use strict';
exports = module.exports = shell;
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
child_process = require('node:child_process'),
@@ -8,8 +10,6 @@ const assert = require('node:assert'),
safe = require('safetydance'),
_ = require('./underscore.js');
exports = module.exports = shell;
function shell(tag) {
assert.strictEqual(typeof tag, 'string');

View File

@@ -1,5 +1,30 @@
'use strict';
exports = module.exports = {
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
getAvailableSize,
getStatus,
upload,
download,
copy,
copyDir,
exists,
listDir,
remove,
removeDir,
};
const assert = require('node:assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:storage/filesystem'),
@@ -394,27 +419,3 @@ function injectPrivateFields(newConfig, currentConfig) {
if (currentConfig._managedMountPath) newConfig._managedMountPath = currentConfig._managedMountPath;
}
exports = module.exports = {
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
getAvailableSize,
getStatus,
upload,
download,
copy,
copyDir,
exists,
listDir,
remove,
removeDir,
};

View File

@@ -1,5 +1,30 @@
'use strict';
exports = module.exports = {
getAvailableSize,
getStatus,
upload,
exists,
download,
copy,
copyDir,
listDir,
remove,
removeDir,
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
};
const assert = require('node:assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
@@ -258,27 +283,3 @@ function injectPrivateFields(newConfig, currentConfig) {
if (!Object.hasOwn(newConfig.credentials, 'private_key') && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
exports = module.exports = {
getAvailableSize,
getStatus,
upload,
exists,
download,
copy,
copyDir,
listDir,
remove,
removeDir,
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
};

View File

@@ -1,5 +1,32 @@
'use strict';
exports = module.exports = {
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
getAvailableSize,
getStatus,
upload,
exists,
download,
copy,
copyDir,
listDir,
remove,
removeDir,
// Used to mock AWS
_chunk: chunk
};
const assert = require('node:assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
@@ -662,29 +689,3 @@ function injectPrivateFields(newConfig, currentConfig) {
newConfig._provider = currentConfig._provider;
}
exports = module.exports = {
setup,
teardown,
cleanup,
verifyConfig,
removePrivateFields,
injectPrivateFields,
getAvailableSize,
getStatus,
upload,
exists,
download,
copy,
copyDir,
listDir,
remove,
removeDir,
// Used to mock AWS
_chunk: chunk
};

View File

@@ -1,5 +1,10 @@
'use strict';
exports = module.exports = {
sync,
finalize
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
DataLayout = require('./datalayout.js'),
@@ -10,11 +15,6 @@ const assert = require('node:assert'),
safe = require('safetydance'),
util = require('node:util');
exports = module.exports = {
sync,
finalize
};
function readCache(cacheFile) {
assert.strictEqual(typeof cacheFile, 'string');

View File

@@ -1,5 +1,54 @@
'use strict';
exports = module.exports = {
get,
add,
update,
setCompleted,
setCompletedByType,
list,
getLogs,
startTask,
stopTask,
stopAllTasks,
removePrivateFields,
_del: del,
// task types. if you add a task here, fill up the function table in taskworker and dashboard constants.js
// '_' prefix is removed for lookup
TASK_APP: 'app',
// "prefix" allows us to locate the tasks of a specific app or backup site
TASK_APP_BACKUP_PREFIX: 'appBackup_',
TASK_FULL_BACKUP_PREFIX: 'backup_', // full backup
TASK_CLEAN_BACKUPS_PREFIX: 'cleanBackups_',
TASK_BOX_UPDATE: 'boxUpdate',
TASK_CHECK_CERTS: 'checkCerts',
TASK_SYNC_DYNDNS: 'syncDyndns',
TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation',
TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
TASK_SYNC_DNS_RECORDS: 'syncDnsRecords',
TASK_CHECK_BACKUP_INTEGRITY: 'checkBackupIntegrity',
// error codes
ESTOPPED: 'stopped',
ECRASHED: 'crashed',
ETIMEOUT: 'timeout',
// testing
_TASK_IDENTITY: 'identity',
_TASK_CRASH: 'crash',
_TASK_ERROR: 'error',
_TASK_SLEEP: 'sleep'
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
@@ -243,51 +292,3 @@ async function del(id) {
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Task not found');
}
exports = module.exports = {
get,
add,
update,
setCompleted,
setCompletedByType,
list,
getLogs,
startTask,
stopTask,
stopAllTasks,
removePrivateFields,
_del: del,
// task types. if you add a task here, fill up the function table in taskworker and dashboard constants.js
// '_' prefix is removed for lookup
TASK_APP: 'app',
// "prefix" allows us to locate the tasks of a specific app or backup site
TASK_APP_BACKUP_PREFIX: 'appBackup_',
TASK_FULL_BACKUP_PREFIX: 'backup_', // full backup
TASK_CLEAN_BACKUPS_PREFIX: 'cleanBackups_',
TASK_BOX_UPDATE: 'boxUpdate',
TASK_CHECK_CERTS: 'checkCerts',
TASK_SYNC_DYNDNS: 'syncDyndns',
TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation',
TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
TASK_SYNC_DNS_RECORDS: 'syncDnsRecords',
TASK_CHECK_BACKUP_INTEGRITY: 'checkBackupIntegrity',
// error codes
ESTOPPED: 'stopped',
ECRASHED: 'crashed',
ETIMEOUT: 'timeout',
// testing
_TASK_IDENTITY: 'identity',
_TASK_CRASH: 'crash',
_TASK_ERROR: 'error',
_TASK_SLEEP: 'sleep'
};

View File

@@ -68,14 +68,6 @@ exports = module.exports = {
compareRoles,
};
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(',');
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
const appPasswords = require('./apppasswords.js'),
assert = require('node:assert'),
BoxError = require('./boxerror.js'),
@@ -105,6 +97,12 @@ const appPasswords = require('./apppasswords.js'),
validator = require('./validator.js'),
_ = require('./underscore.js');
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(',');
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
const CRYPTO_SALT_SIZE = 64; // 512-bit salt
const CRYPTO_ITERATIONS = 10000; // iterations
const CRYPTO_KEY_LENGTH = 512; // bits
@@ -201,6 +199,28 @@ function removePrivateFields(user) {
return result;
}
function validateRole(role) {
assert.strictEqual(typeof role, 'string');
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
if (ORDERED_ROLES.includes(role)) return null;
return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`);
}
function compareRoles(role1, role2) {
assert.strictEqual(typeof role1, 'string');
assert.strictEqual(typeof role2, 'string');
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
const roleInt1 = ORDERED_ROLES.indexOf(role1);
const roleInt2 = ORDERED_ROLES.indexOf(role2);
return roleInt1 - roleInt2;
}
async function add(email, data, auditSource) {
assert.strictEqual(typeof email, 'string');
assert(data && typeof data === 'object');
@@ -290,181 +310,6 @@ async function add(email, data, auditSource) {
return user.id;
}
async function setGhost(user, password, expiresAt) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof expiresAt, 'number');
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
ghostData[user.username] = { password, expiresAt };
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
}
// returns true if ghost user was matched
async function verifyGhost(username, password) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
if (username in ghostData) {
if (typeof ghostData[username] === 'object') {
if (ghostData[username].expiresAt < Date.now()) {
debug('verifyGhost: password expired');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return false;
} else if (ghostData[username].password === password) {
debug('verifyGhost: matched ghost user');
return true;
} else {
return false;
}
} else if(ghostData[username] === password) {
debug('verifyGhost: matched ghost user');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return true;
}
}
return false;
}
async function verifyAppPassword(userId, password, identifier) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
const results = await appPasswords.list(userId);
const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword);
const hash = crypto.createHash('sha256').update(password).digest('base64');
if (hashedPasswords.includes(hash)) return;
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid');
}
// identifier is only used to check if password is valid for a specific app
async function verify(user, password, identifier, options) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
if (!user.active) {
debug(`verify: ${user.username} is not active`);
throw new BoxError(BoxError.NOT_FOUND, 'User not active');
}
// for just invited users the username may be still null
if (user.username) {
const valid = await verifyGhost(user.username, password);
if (valid) {
debug(`verify: ${user.username} authenticated via impersonation`);
user.ghost = true;
return user;
}
}
const [error] = await safe(verifyAppPassword(user.id, password, identifier));
if (!error) { // matched app password
debug(`verify: ${user.username || user.id} matched app password`);
user.appPassword = true;
return user;
}
let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds
if (user.source === 'ldap') {
await externalLdap.verifyPassword(user.username, password, options);
const externalLdapConfig = await externalLdap.getConfig();
localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
} else {
const saltBinary = Buffer.from(user.salt, 'hex');
const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error);
const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
if (derivedKeyHex !== user.password) {
debug(`verify: ${user.username || user.id} provided incorrect password`);
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
}
localTotpCheck = user.twoFactorAuthenticationEnabled;
}
if (localTotpCheck && !options.skipTotpCheck) {
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
}
return user;
}
async function verifyWithId(id, password, identifier, options) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await get(id);
if (!user) {
debug(`verifyWithId: ${id} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
async function verifyWithUsername(username, password, identifier, options) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByUsername(username.toLowerCase());
if (user) return await verify(user, password, identifier, options);
const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
if (error && error.reason === BoxError.BAD_STATE) {
debug(`verifyWithUsername: ${username} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create
}
if (error) {
debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verifyWithId(newUserId, password, identifier, options);
}
async function verifyWithEmail(email, password, identifier, options) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByEmail(email.toLowerCase());
if (!user) {
debug(`verifyWithEmail: ${email} no such user`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
async function del(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert(auditSource && typeof auditSource === 'object');
@@ -712,6 +557,181 @@ async function getSuperadmins() {
return await listByRole(exports.ROLE_OWNER);
}
async function setGhost(user, password, expiresAt) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof expiresAt, 'number');
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
ghostData[user.username] = { password, expiresAt };
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
}
// returns true if ghost user was matched
async function verifyGhost(username, password) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
if (username in ghostData) {
if (typeof ghostData[username] === 'object') {
if (ghostData[username].expiresAt < Date.now()) {
debug('verifyGhost: password expired');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return false;
} else if (ghostData[username].password === password) {
debug('verifyGhost: matched ghost user');
return true;
} else {
return false;
}
} else if(ghostData[username] === password) {
debug('verifyGhost: matched ghost user');
delete ghostData[username];
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
return true;
}
}
return false;
}
async function verifyAppPassword(userId, password, identifier) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
const results = await appPasswords.list(userId);
const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword);
const hash = crypto.createHash('sha256').update(password).digest('base64');
if (hashedPasswords.includes(hash)) return;
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid');
}
// identifier is only used to check if password is valid for a specific app
async function verify(user, password, identifier, options) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
if (!user.active) {
debug(`verify: ${user.username} is not active`);
throw new BoxError(BoxError.NOT_FOUND, 'User not active');
}
// for just invited users the username may be still null
if (user.username) {
const valid = await verifyGhost(user.username, password);
if (valid) {
debug(`verify: ${user.username} authenticated via impersonation`);
user.ghost = true;
return user;
}
}
const [error] = await safe(verifyAppPassword(user.id, password, identifier));
if (!error) { // matched app password
debug(`verify: ${user.username || user.id} matched app password`);
user.appPassword = true;
return user;
}
let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds
if (user.source === 'ldap') {
await externalLdap.verifyPassword(user.username, password, options);
const externalLdapConfig = await externalLdap.getConfig();
localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
} else {
const saltBinary = Buffer.from(user.salt, 'hex');
const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error);
const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
if (derivedKeyHex !== user.password) {
debug(`verify: ${user.username || user.id} provided incorrect password`);
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
}
localTotpCheck = user.twoFactorAuthenticationEnabled;
}
if (localTotpCheck && !options.skipTotpCheck) {
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
}
return user;
}
async function verifyWithId(id, password, identifier, options) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await get(id);
if (!user) {
debug(`verifyWithId: ${id} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
async function verifyWithUsername(username, password, identifier, options) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByUsername(username.toLowerCase());
if (user) return await verify(user, password, identifier, options);
const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
if (error && error.reason === BoxError.BAD_STATE) {
debug(`verifyWithUsername: ${username} not found`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create
}
if (error) {
debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verifyWithId(newUserId, password, identifier, options);
}
async function verifyWithEmail(email, password, identifier, options) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof options, 'object');
const user = await getByEmail(email.toLowerCase());
if (!user) {
debug(`verifyWithEmail: ${email} no such user`);
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
}
return await verify(user, password, identifier, options);
}
async function getPasswordResetLink(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -969,24 +989,6 @@ async function disableTwoFactorAuthentication(user, auditSource) {
await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource);
}
function validateRole(role) {
assert.strictEqual(typeof role, 'string');
if (ORDERED_ROLES.indexOf(role) !== -1) return null;
return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`);
}
function compareRoles(role1, role2) {
assert.strictEqual(typeof role1, 'string');
assert.strictEqual(typeof role2, 'string');
const roleInt1 = ORDERED_ROLES.indexOf(role1);
const roleInt2 = ORDERED_ROLES.indexOf(role2);
return roleInt1 - roleInt2;
}
async function getAvatar(user) {
assert.strictEqual(typeof user, 'object');
@@ -1042,3 +1044,4 @@ function parseDisplayName(displayName) {
const lastName = displayName.substring(idx+1);
return { firstName, lastName, middleName };
}

View File

@@ -1,5 +1,21 @@
'use strict';
exports = module.exports = {
add,
get,
update,
del,
list,
remount,
getStatus,
removePrivateFields,
mountAll,
// exported for testing
_validateHostPath: validateHostPath
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
crypto = require('node:crypto'),
@@ -202,18 +218,3 @@ async function mountAll() {
}
}
exports = module.exports = {
add,
get,
update,
del,
list,
remount,
getStatus,
removePrivateFields,
mountAll,
// exported for testing
_validateHostPath: validateHostPath
};