diff --git a/src/apps.js b/src/apps.js index f43e31a1f..b6c185794 100644 --- a/src/apps.js +++ b/src/apps.js @@ -595,9 +595,9 @@ function downloadManifest(appStoreId, manifest, callback) { superagent.get(url).timeout(30 * 1000).end(function (error, result) { if (error && !error.response) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text))); + if (result.status !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.status, result.text))); - if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.statusCode, result.text))); + if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.status, result.text))); callback(null, parts[0], result.body.manifest); }); @@ -1611,7 +1611,9 @@ function purchaseApp(data, callback) { assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof callback, 'function'); - appstore.purchaseApp(data, function (error) { + const purchaseApp = util.callbackify(appstore.purchaseApp); + + purchaseApp(data, function (error) { if (!error) return callback(); // if purchase failed, rollback the appdb record @@ -1733,7 +1735,9 @@ function uninstall(app, auditSource, callback) { let error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL); if (error) return callback(error); - appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) { + const unpurchaseApp = util.callbackify(appstore.unpurchaseApp); + + unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' }, function (error) { if (error) return callback(error); const task = { diff --git a/src/appstore.js b/src/appstore.js index 9e12980a3..956ae6cd2 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -13,7 +13,7 @@ exports = module.exports = { purchaseApp, unpurchaseApp, - getUserToken, + createUserToken, getSubscription, isFreePlan, @@ -23,9 +23,8 @@ exports = module.exports = { createTicket }; -var apps = require('./apps.js'), +const apps = require('./apps.js'), assert = require('assert'), - async = require('async'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:appstore'), @@ -74,99 +73,89 @@ function isAppAllowed(appstoreId, listingConfig) { return true; } -function getCloudronToken(callback) { - assert.strictEqual(typeof callback, 'function'); - - settings.getCloudronToken(function (error, token) { - if (error) return callback(error); - if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token')); - - callback(null, token); - }); +async function getCloudronToken() { + const token = await settings.getCloudronToken(); + if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token'); + return token; } -function login(email, password, totpToken, callback) { +async function login(email, password, totpToken) { assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof totpToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - var data = { - email: email, - password: password, - totpToken: totpToken - }; + const data = { email, password, totpToken }; const url = settings.apiServerOrigin() + '/api/v1/login'; - superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`)); + const [error, response] = await safe(superagent.post(url) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); - callback(null, result.body); // { userId, accessToken } - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${response.status}`); + + return response.body; // { userId, accessToken } } -function registerUser(email, password, callback) { +async function registerUser(email, password) { assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof callback, 'function'); - var data = { - email: email, - password: password, - }; + const data = { email, password }; const url = settings.apiServerOrigin() + '/api/v1/register_user'; - superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message)); - if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`)); + const [error, response] = await safe(superagent.post(url) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); - callback(null); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + + if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, error.message); + if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${response.status}`); } -function getUserToken(callback) { - assert.strictEqual(typeof callback, 'function'); +async function createUserToken() { + if (settings.isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); - if (settings.isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode')); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/user_token`; - getCloudronToken(function (error, token) { - if (error) return callback(error); + const [error, response] = await safe(superagent.post(url) + .send({}) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - const url = `${settings.apiServerOrigin()}/api/v1/user_token`; + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${response.status}`); - superagent.post(url).send({}).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${result.status}`)); - - callback(null, result.body.accessToken); - }); - }); + return response.body.accessToken; } -function getSubscription(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getSubscription() { + const token = await getCloudronToken(); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const url = settings.apiServerOrigin() + '/api/v1/subscription'; - const url = settings.apiServerOrigin() + '/api/v1/subscription'; - superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR)); - if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`)); + const [error, response] = await safe(superagent.get(url) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - // update the features cache - gFeatures = result.body.features; - safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8'); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR); + if (response.status === 502) throw new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`); + if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`); - callback(null, result.body); - }); - }); + // update the features cache + gFeatures = response.body.features; + safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8'); + + return response.body; } function isFreePlan(subscription) { @@ -174,225 +163,203 @@ function isFreePlan(subscription) { } // See app.js install it will create a db record first but remove it again if appstore purchase fails -function purchaseApp(data, callback) { +async function purchaseApp(data) { assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId } assert(data.appstoreId || data.manifestId); assert.strictEqual(typeof data.appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`; - const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`; + const [error, response] = await safe(superagent.post(url) + .send(data) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - // 200 if already purchased, 201 is newly purchased - if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body))); - - callback(null); - }); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND); // appstoreId does not exist + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 402) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + // 200 if already purchased, 201 is newly purchased + if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', response.status, response.body)); } -function unpurchaseApp(appId, data, callback) { +async function unpurchaseApp(appId, data) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId } assert(data.appstoreId || data.manifestId); - assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`; - const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`; + let [error, response] = await safe(superagent.get(url) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 404) return callback(null); // was never purchased - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body))); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 404) return; // was never purchased + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', response.status, response.body)); - superagent.del(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body))); + [error, response] = await safe(superagent.del(url) + .send(data) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - callback(null); - }); - }); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', response.status, response.body)); } -function getBoxUpdate(options, callback) { +async function getBoxUpdate(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`; - const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`; + const query = { + accessToken: token, + boxVersion: constants.VERSION, + automatic: options.automatic + }; - const query = { - accessToken: token, - boxVersion: constants.VERSION, - automatic: options.automatic - }; + const [error, response] = await safe(superagent.get(url) + .query(query) + .timeout(30 * 1000) + .ok(() => true)); - superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode === 204) return callback(null, null); // no update - if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status === 204) return; // no update + if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text)); - var updateInfo = result.body; + const updateInfo = response.body; - if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) { - return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', result.statusCode, result.text))); - } + if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) { + throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', response.status, response.text)); + } - // updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl } - if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text))); - if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text))); - if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text))); - if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text))); - if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text))); - if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text))); + // updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl } + if (!updateInfo.version || typeof updateInfo.version !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text)); + if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text)); + if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', response.status, response.text)); + if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', response.status, response.text)); + if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', response.status, response.text)); + if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', response.status, response.text)); - callback(null, updateInfo); - }); - }); + return updateInfo; } -function getAppUpdate(app, options, callback) { +async function getAppUpdate(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/appupdate`; + const query = { + accessToken: token, + boxVersion: constants.VERSION, + appId: app.appStoreId, + appVersion: app.manifest.version, + automatic: options.automatic + }; - const url = `${settings.apiServerOrigin()}/api/v1/appupdate`; - const query = { - accessToken: token, - boxVersion: constants.VERSION, - appId: app.appStoreId, - appVersion: app.manifest.version, - automatic: options.automatic - }; + const [error, response] = await safe(superagent.get(url) + .query(query) + .timeout(30 * 1000) + .ok(() => true)); - superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode === 204) return callback(null); // no update - if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status === 204) return; // no update + if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text)); - const updateInfo = result.body; + const updateInfo = response.body; - // for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0 - const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; + // for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0 + const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`; - // do some sanity checks - if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) { - debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo); - return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text))); - } + // do some sanity checks + if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) { + debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo); + throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', response.status, response.text)); + } - updateInfo.unstable = !!updateInfo.unstable; + updateInfo.unstable = !!updateInfo.unstable; - // { id, creationDate, manifest, unstable } - callback(null, updateInfo); - }); - }); + // { id, creationDate, manifest, unstable } + return updateInfo; } -function registerCloudron(data, callback) { +async function registerCloudron(data) { assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof callback, 'function'); const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`; - superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`)); + const [error, response] = await safe(superagent.post(url) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); - // cloudronId, token, licenseKey - if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id')); - if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token')); - if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license')); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.statusCode} ${error.message}`); - async.series([ - settings.setCloudronId.bind(null, result.body.cloudronId), - settings.setCloudronToken.bind(null, result.body.cloudronToken), - settings.setLicenseKey.bind(null, result.body.licenseKey), - ], function (error) { - if (error) return callback(error); + // cloudronId, token, licenseKey + if (!response.body.cloudronId) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'); + if (!response.body.cloudronToken) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'); + if (!response.body.licenseKey) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'); - debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`); + await settings.setCloudronId(response.body.cloudronId); + await settings.setCloudronToken(response.body.cloudronToken); + await settings.setLicenseKey(response.body.licenseKey); - callback(); - }); - }); + debug(`registerCloudron: Cloudron registered with id ${response.body.cloudronId}`); } -function updateCloudron(data, callback) { +async function updateCloudron(data) { assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - getCloudronToken(function (error, token) { - if (error && error.reason === BoxError.LICENSE_ERROR) return callback(null); // missing token. not registered yet - if (error) return callback(error); + const token = await getCloudronToken(); + const url = `${settings.apiServerOrigin()}/api/v1/update_cloudron`; + const query = { + accessToken: token + }; - const url = `${settings.apiServerOrigin()}/api/v1/update_cloudron`; - const query = { - accessToken: token - }; + const [error, response] = await safe(superagent.post(url) + .query(query) + .send(data) + .timeout(30 * 1000) + .ok(() => true)); - superagent.post(url).query(query).send(data).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text)); - debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`); - - callback(); - }); - }); + debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`); } -function registerWithLoginCredentials(options, callback) { +async function registerWithLoginCredentials(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - function maybeSignup(done) { - if (!options.signup) return done(); + const token = await getCloudronToken(); + if (token) throw new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'); - registerUser(options.email, options.password, done); - } - - getCloudronToken(function (error, token) { - if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered')); - - maybeSignup(function (error) { - if (error) return callback(error); - - login(options.email, options.password, options.totpToken || '', function (error, result) { - if (error) return callback(error); - - registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION }, callback); - }); - }); - }); + if (!options.signup) return; + await registerUser(options.email, options.password); + const result = await login(options.email, options.password, options.totpToken || ''); + await registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION }); } -function createTicket(info, auditSource, callback) { +async function createTicket(info, auditSource) { assert.strictEqual(typeof info, 'object'); assert.strictEqual(typeof info.email, 'string'); assert.strictEqual(typeof info.displayName, 'string'); @@ -400,127 +367,96 @@ function createTicket(info, auditSource, callback) { assert.strictEqual(typeof info.subject, 'string'); assert.strictEqual(typeof info.description, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - function collectAppInfoIfNeeded(callback) { - if (!info.appId) return callback(); - apps.get(info.appId, callback); + const token = await getCloudronToken(); + + if (info.enableSshSupport) { + await safe(support.enableRemoteSupport(true, auditSource)); } - function enableSshIfNeeded(callback) { - if (!info.enableSshSupport) return callback(); + info.app = info.appId ? await util.promisify(apps.get)(info.appId) : null; + info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets - support.enableRemoteSupport(true, auditSource, function (error) { - // ensure we can at least get the ticket through - if (error) debug('Unable to enable SSH support.', error); + const request = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true); - callback(); + // either send as JSON through body or as multipart, depending on attachments + if (info.app) { + request.field('infoJSON', JSON.stringify(info)); + + apps.getLocalLogfilePaths(info.app).forEach(function (filePath) { + const logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`); + if (logs) request.attach(path.basename(filePath), logs, path.basename(filePath)); }); + } else { + request.send(info); } - getCloudronToken(function (error, token) { - if (error) return callback(error); + const [error, response] = await safe(request); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text)); - enableSshIfNeeded(function (error) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info); - collectAppInfoIfNeeded(function (error, app) { - if (error) return callback(error); - if (app) info.app = app; - - info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets - - var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`) - .query({ accessToken: token }) - .timeout(30 * 1000); - - // either send as JSON through body or as multipart, depending on attachments - if (info.app) { - req.field('infoJSON', JSON.stringify(info)); - - apps.getLocalLogfilePaths(info.app).forEach(function (filePath) { - var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`); - if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath)); - }); - } else { - req.send(info); - } - - req.end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); - - eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info); - - callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` }); - }); - }); - }); - }); + return { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` }; } -function getApps(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getApps() { + const token = await getCloudronToken(); - getCloudronToken(async function (error, token) { - if (error) return callback(error); + const unstable = await settings.getUnstableAppsConfig(); - const [settingsError, unstable] = await settings.getUnstableAppsConfig(); - if (settingsError) return callback(settingsError); + const url = `${settings.apiServerOrigin()}/api/v1/apps`; - const url = `${settings.apiServerOrigin()}/api/v1/apps`; - superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body))); - if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); + const [error, response] = await safe(superagent.get(url) + .query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }) + .timeout(30 * 1000) + .ok(() => true)); - settings.getAppstoreListingConfig(function (error, listingConfig) { - if (error) return callback(error); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', response.status, response.body)); + if (!response.body.apps) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text)); - const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig)); - - callback(null, filteredApps); - }); - }); - }); + const listingConfig = await settings.getAppstoreListingConfig(); + const filteredApps = response.body.apps.filter(app => isAppAllowed(app.id, listingConfig)); + return filteredApps; } -function getAppVersion(appId, version, callback) { +async function getAppVersion(appId, version) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof version, 'string'); - assert.strictEqual(typeof callback, 'function'); - settings.getAppstoreListingConfig(function (error, listingConfig) { - if (error) return callback(error); + const listingConfig = await settings.getAppstoreListingConfig(); - if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED)); + if (!isAppAllowed(appId, listingConfig)) throw new BoxError(BoxError.FEATURE_DISABLED); - getCloudronToken(function (error, token) { - if (error) return callback(error); + const token = await getCloudronToken(); - let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`; - if (version !== 'latest') url += `/versions/${version}`; + let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`; + if (version !== 'latest') url += `/versions/${version}`; - superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) { - if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message)); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); - if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message)); - if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body))); + const [error, response] = await safe(superagent.get(url) + .query({ accessToken: token }) + .timeout(30 * 1000) + .ok(() => true)); - callback(null, result.body); - }); - }); - }); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message); + if (response.status === 403 || response.statusCode === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS); + if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND); + if (response.status === 422) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message); + if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', response.status, response.body)); + + return response.body; } -function getApp(appId, callback) { +async function getApp(appId) { assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - getAppVersion(appId, 'latest', callback); + return await getAppVersion(appId, 'latest'); } diff --git a/src/cloudron.js b/src/cloudron.js index 3e602ace4..376337849 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -314,10 +314,10 @@ function setDashboardDomain(domain, auditSource, callback) { const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject); - settings.setDashboardLocation(domain, fqdn, function (error) { + settings.setDashboardLocation(domain, fqdn, async function (error) { if (error) return callback(error); - appstore.updateCloudron({ domain }, NOOP_CALLBACK); + await safe(appstore.updateCloudron({ domain })); eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn }); diff --git a/src/cron.js b/src/cron.js index 11454003e..b79654f4d 100644 --- a/src/cron.js +++ b/src/cron.js @@ -81,7 +81,7 @@ async function startJobs() { // this is run separately from the update itself so that the user can disable automatic updates but can still get a notification gJobs.updateCheckerJob = new CronJob({ cronTime: `${randomTick} ${randomTick} 1,5,9,13,17,21,23 * * *`, - onTick: () => updateChecker.checkForUpdates({ automatic: true }, NOOP_CALLBACK), + onTick: async () => await updateChecker.checkForUpdates({ automatic: true }), start: true }); diff --git a/src/mailer.js b/src/mailer.js index f83b03fea..351220f2a 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -35,17 +35,15 @@ const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); function getMailConfig(callback) { assert.strictEqual(typeof callback, 'function'); - settings.getCloudronName(function (error, cloudronName) { + settings.getCloudronName(async function (error, cloudronName) { if (error) debug('Error getting cloudron name: ', error); - settings.getSupportConfig(function (error, supportConfig) { - if (error) debug('Error getting support config: ', error); + const supportConfig = await settings.getSupportConfig(); - callback(null, { - cloudronName: cloudronName || '', - notificationFrom: `"${cloudronName}" `, - supportEmail: supportConfig.email - }); + callback(null, { + cloudronName: cloudronName || '', + notificationFrom: `"${cloudronName}" `, + supportEmail: supportConfig.email }); }); } diff --git a/src/routes/appstore.js b/src/routes/appstore.js index 4faa506df..c7a95551b 100644 --- a/src/routes/appstore.js +++ b/src/routes/appstore.js @@ -10,50 +10,47 @@ exports = module.exports = { getSubscription }; -var appstore = require('../appstore.js'), +const appstore = require('../appstore.js'), assert = require('assert'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess; + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'); -function getApps(req, res, next) { - appstore.getApps(function (error, apps) { - if (error) return next(BoxError.toHttpError(error)); +async function getApps(req, res, next) { + const [error, apps] = await safe(appstore.getApps()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { apps })); - }); + next(new HttpSuccess(200, { apps })); } -function getApp(req, res, next) { +async function getApp(req, res, next) { assert.strictEqual(typeof req.params.appstoreId, 'string'); - appstore.getApp(req.params.appstoreId, function (error, app) { - if (error) return next(BoxError.toHttpError(error)); + const [error, app] = await safe(appstore.getApp(req.params.appstoreId)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, app)); - }); + next(new HttpSuccess(200, app)); } -function getAppVersion(req, res, next) { +async function getAppVersion(req, res, next) { assert.strictEqual(typeof req.params.appstoreId, 'string'); assert.strictEqual(typeof req.params.versionId, 'string'); - appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) { - if (error) return next(BoxError.toHttpError(error)); + const [error, manifest] = await safe(appstore.getAppVersion(req.params.appstoreId, req.params.versionId)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, manifest)); - }); + next(new HttpSuccess(200, manifest)); } -function createUserToken(req, res, next) { - appstore.getUserToken(function (error, result) { - if (error) return next(BoxError.toHttpError(error)); +async function createUserToken(req, res, next) { + const [error, accessToken] = await safe(appstore.createUserToken()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, { accessToken: result })); - }); + next(new HttpSuccess(201, { accessToken })); } -function registerCloudron(req, res, next) { +async function registerCloudron(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.email !== 'string' || !req.body.email) return next(new HttpError(400, 'email must be string')); @@ -61,19 +58,17 @@ function registerCloudron(req, res, next) { if ('totpToken' in req.body && typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be string')); if (typeof req.body.signup !== 'boolean') return next(new HttpError(400, 'signup must be a boolean')); - appstore.registerWithLoginCredentials(req.body, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(appstore.registerWithLoginCredentials(req.body)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); - }); + next(new HttpSuccess(201, {})); } -function getSubscription(req, res, next) { +async function getSubscription(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - appstore.getSubscription(function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(appstore.getSubscription()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features } - }); + next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features } } diff --git a/src/routes/branding.js b/src/routes/branding.js index 3493c9018..71e4390d3 100644 --- a/src/routes/branding.js +++ b/src/routes/branding.js @@ -55,7 +55,7 @@ function getCloudronName(req, res, next) { }); } -function setAppstoreListingConfig(req, res, next) { +async function setAppstoreListingConfig(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const listingConfig = _.pick(req.body, 'whitelist', 'blacklist'); @@ -73,19 +73,17 @@ function setAppstoreListingConfig(req, res, next) { if (!listingConfig.blacklist.every(id => typeof id === 'string')) return next(new HttpError(400, 'blacklist must be array of strings')); } - settings.setAppstoreListingConfig(listingConfig, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(settings.setAppstoreListingConfig(listingConfig)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function getAppstoreListingConfig(req, res, next) { - settings.getAppstoreListingConfig(function (error, listingConfig) { - if (error) return next(BoxError.toHttpError(error)); +async function getAppstoreListingConfig(req, res, next) { + const [error, listingConfig] = await safe(settings.getAppstoreListingConfig()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, listingConfig)); - }); + next(new HttpSuccess(200, listingConfig)); } async function setCloudronAvatar(req, res, next) { diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index e0c89b167..b575f5b69 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -196,13 +196,12 @@ function getUpdateInfo(req, res, next) { next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); } -function checkForUpdates(req, res, next) { +async function checkForUpdates(req, res, next) { // it can take a while sometimes to get all the app updates one by one req.clearTimeout(); - updateChecker.checkForUpdates({ automatic: false }, function () { - next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); - }); + await updateChecker.checkForUpdates({ automatic: false }); + next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); } function getLogs(req, res, next) { diff --git a/src/routes/profile.js b/src/routes/profile.js index 0962fb63c..a384b8844 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -26,7 +26,7 @@ const assert = require('assert'), async function authorize(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - const [error, directoryConfig] = await settings.getDirectoryConfig(); + const [error, directoryConfig] = await safe(settings.getDirectoryConfig()); if (error) return next(BoxError.toHttpError(error)); if (directoryConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles')); diff --git a/src/routes/settings.js b/src/routes/settings.js index 4360c9890..d958f96f9 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -58,12 +58,11 @@ function setTimeZone(req, res, next) { }); } -function getSupportConfig(req, res, next) { - settings.getSupportConfig(function (error, supportConfig) { - if (error) return next(BoxError.toHttpError(error)); +async function getSupportConfig(req, res, next) { + const [error, supportConfig] = await settings.getSupportConfig(); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, supportConfig)); - }); + next(new HttpSuccess(200, supportConfig)); } function getBackupConfig(req, res, next) { diff --git a/src/routes/support.js b/src/routes/support.js index 0c502a4f5..7baa8beb3 100644 --- a/src/routes/support.js +++ b/src/routes/support.js @@ -10,27 +10,27 @@ exports = module.exports = { canEnableRemoteSupport }; -var appstore = require('../appstore.js'), +const appstore = require('../appstore.js'), assert = require('assert'), auditSource = require('../auditsource.js'), constants = require('../constants.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'), settings = require('../settings.js'), support = require('../support.js'), _ = require('underscore'); -function canCreateTicket(req, res, next) { - settings.getSupportConfig(function (error, supportConfig) { - if (error) return next(new HttpError(503, error.message)); +async function canCreateTicket(req, res, next) { + const [error, supportConfig] = await safe(settings.getSupportConfig()); + if (error) return next(new HttpError(503, error.message)); - if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin')); + if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin')); - next(); - }); + next(); } -function createTicket(req, res, next) { +async function createTicket(req, res, next) { assert.strictEqual(typeof req.user, 'object'); const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request', 'email_error' ]; @@ -43,44 +43,39 @@ function createTicket(req, res, next) { if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string')); if (req.body.enableSshSupport && typeof req.body.enableSshSupport !== 'boolean') return next(new HttpError(400, 'enableSshSupport must be a boolean')); - settings.getSupportConfig(function (error, supportConfig) { - if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`)); - if (supportConfig.email !== constants.SUPPORT_EMAIL) return next(new HttpError(503, 'Sending to non-cloudron email not implemented yet')); + const [error, supportConfig] = await safe(settings.getSupportConfig()); + if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`)); + if (supportConfig.email !== constants.SUPPORT_EMAIL) return next(new HttpError(503, 'Sending to non-cloudron email not implemented yet')); - appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error, result) { - if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`)); + const [ticketError, result] = await safe(appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req))); + if (ticketError) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`)); - next(new HttpSuccess(201, result)); - }); - }); + next(new HttpSuccess(201, result)); } -function canEnableRemoteSupport(req, res, next) { - settings.getSupportConfig(function (error, supportConfig) { - if (error) return next(new HttpError(503, error.message)); +async function canEnableRemoteSupport(req, res, next) { + const [error, supportConfig] = await safe(settings.getSupportConfig()); + if (error) return next(new HttpError(503, error.message)); - if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin')); + if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin')); - next(); - }); + next(); } -function enableRemoteSupport(req, res, next) { +async function enableRemoteSupport(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required')); - support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req), function (error) { - if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server')); + const [error] = await safe(support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req))); + if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server')); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function getRemoteSupport(req, res, next) { - support.getRemoteSupport(function (error, status) { - if (error) return next(new HttpError(500, error)); +async function getRemoteSupport(req, res, next) { + const [error, enabled] = await safe(support.getRemoteSupport()); + if (error) return next(new HttpError(500, error)); - next(new HttpSuccess(200, status)); - }); + next(new HttpSuccess(200, { enabled })); } diff --git a/src/settings.js b/src/settings.js index 4751d475c..9b5292623 100644 --- a/src/settings.js +++ b/src/settings.js @@ -384,7 +384,7 @@ function setDynamicDnsConfig(enabled, callback) { async function getUnstableAppsConfig() { const result = await get(exports.UNSTABLE_APPS_KEY); - if (result === null) gDefaults[exports.UNSTABLE_APPS_KEY]; + if (result === null) return gDefaults[exports.UNSTABLE_APPS_KEY]; return !!result; // db holds string values only } @@ -650,28 +650,18 @@ async function setDirectoryConfig(directoryConfig) { notifyChange(exports.DIRECTORY_CONFIG_KEY, directoryConfig); } -function getAppstoreListingConfig(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getAppstoreListingConfig() { + const value = await get(exports.APPSTORE_LISTING_CONFIG_KEY); + if (value === null) return gDefaults[exports.APPSTORE_LISTING_CONFIG_KEY]; - settingsdb.get(exports.APPSTORE_LISTING_CONFIG_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.APPSTORE_LISTING_CONFIG_KEY]); - if (error) return callback(error); - - callback(null, JSON.parse(value)); - }); + return JSON.parse(value); } -function setAppstoreListingConfig(listingConfig, callback) { +async function setAppstoreListingConfig(listingConfig) { assert.strictEqual(typeof listingConfig, 'object'); - assert.strictEqual(typeof callback, 'function'); - settingsdb.set(exports.APPSTORE_LISTING_CONFIG_KEY, JSON.stringify(listingConfig), function (error) { - if (error) return callback(error); - - notifyChange(exports.APPSTORE_LISTING_CONFIG_KEY, listingConfig); - - callback(null); - }); + await set(exports.APPSTORE_LISTING_CONFIG_KEY, JSON.stringify(listingConfig)); + notifyChange(exports.APPSTORE_LISTING_CONFIG_KEY, listingConfig); } async function getFirewallBlocklist() { @@ -688,39 +678,24 @@ async function setFirewallBlocklist(blocklist) { await setBlob(exports.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist)); } -function getSupportConfig(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getSupportConfig() { + const value = await get(exports.SUPPORT_CONFIG_KEY); + if (value === null) return gDefaults[exports.SUPPORT_CONFIG_KEY]; - settingsdb.get(exports.SUPPORT_CONFIG_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.SUPPORT_CONFIG_KEY]); - if (error) return callback(error); - - callback(null, JSON.parse(value)); - }); + return JSON.parse(value); } -function getLicenseKey(callback) { - assert.strictEqual(typeof callback, 'function'); - - settingsdb.get(exports.LICENSE_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.LICENSE_KEY]); - if (error) return callback(error); - - callback(null, value); - }); +async function getLicenseKey() { + const value = get(exports.LICENSE_KEY); + if (value === null) return gDefaults[exports.LICENSE_KEY]; + return value; } -function setLicenseKey(licenseKey, callback) { +async function setLicenseKey(licenseKey) { assert.strictEqual(typeof licenseKey, 'string'); - assert.strictEqual(typeof callback, 'function'); - settingsdb.set(exports.LICENSE_KEY, licenseKey, function (error) { - if (error) return callback(error); - - notifyChange(exports.LICENSE_KEY, licenseKey); - - callback(null); - }); + await set(exports.LICENSE_KEY, licenseKey); + notifyChange(exports.LICENSE_KEY, licenseKey); } function getLanguage(callback) { @@ -753,52 +728,30 @@ function setLanguage(language, callback) { }); } -function getCloudronId(callback) { - assert.strictEqual(typeof callback, 'function'); - - settingsdb.get(exports.CLOUDRON_ID_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_ID_KEY]); - if (error) return callback(error); - - callback(null, value); - }); +async function getCloudronId() { + const value = await get(exports.CLOUDRON_ID_KEY); + if (value === null) return gDefaults[exports.CLOUDRON_ID_KEY]; + return value; } -function setCloudronId(cid, callback) { +async function setCloudronId(cid) { assert.strictEqual(typeof cid, 'string'); - assert.strictEqual(typeof callback, 'function'); - settingsdb.set(exports.CLOUDRON_ID_KEY, cid, function (error) { - if (error) return callback(error); - - notifyChange(exports.CLOUDRON_ID_KEY, cid); - - callback(null); - }); + await set(exports.CLOUDRON_ID_KEY, cid); + notifyChange(exports.CLOUDRON_ID_KEY, cid); } -function getCloudronToken(callback) { - assert.strictEqual(typeof callback, 'function'); - - settingsdb.get(exports.CLOUDRON_TOKEN_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.CLOUDRON_TOKEN_KEY]); - if (error) return callback(error); - - callback(null, value); - }); +async function getCloudronToken() { + const value = await get(exports.CLOUDRON_TOKEN_KEY); + if (value === null) return gDefaults[exports.CLOUDRON_TOKEN_KEY]; + return value; } -function setCloudronToken(token, callback) { +async function setCloudronToken(token) { assert.strictEqual(typeof token, 'string'); - assert.strictEqual(typeof callback, 'function'); - settingsdb.set(exports.CLOUDRON_TOKEN_KEY, token, function (error) { - if (error) return callback(error); - - notifyChange(exports.CLOUDRON_TOKEN_KEY, token); - - callback(null); - }); + await set(exports.CLOUDRON_TOKEN_KEY, token); + notifyChange(exports.CLOUDRON_TOKEN_KEY, token); } async function list() { diff --git a/src/shell.js b/src/shell.js index c0c9d0af2..265060fa6 100644 --- a/src/shell.js +++ b/src/shell.js @@ -53,6 +53,7 @@ function spawn(tag, file, args, options, callback) { debug(tag + ' spawn: %s %s', file, args.join(' ').replace(/\n/g, '\\n')); const cp = child_process.spawn(file, args, options); + let stdoutResult = ''; if (options.logStream) { cp.stdout.pipe(options.logStream); @@ -60,6 +61,7 @@ function spawn(tag, file, args, options, callback) { } else { cp.stdout.on('data', function (data) { debug(tag + ' (stdout): %s', data.toString('utf8')); + stdoutResult += data.toString('utf8'); }); cp.stderr.on('data', function (data) { @@ -69,7 +71,7 @@ function spawn(tag, file, args, options, callback) { cp.on('exit', function (code, signal) { if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal); - if (code === 0) return callback(null); + if (code === 0) return callback(null, stdoutResult); let e = new BoxError(BoxError.SPAWN_ERROR, `${tag} exited with code ${code} signal ${signal}`); e.code = code; diff --git a/src/support.js b/src/support.js index 4d2e1ab5a..cfc8fb2a0 100644 --- a/src/support.js +++ b/src/support.js @@ -11,7 +11,6 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), eventlog = require('./eventlog.js'), - once = require('once'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), @@ -30,38 +29,26 @@ function sshInfo() { filePath = '/home/ubuntu/.ssh/authorized_keys'; user = 'ubuntu'; } else { - filePath = '/root/.ssh/authorized_keys'; user = 'root'; } return { filePath, user }; } -function getRemoteSupport(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getRemoteSupport() { + const [error, stdoutResult] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], {})); + if (error) throw new BoxError(BoxError.FS_ERROR, error); - callback = once(callback); // exit may or may not be called after an 'error' - - let result = ''; - let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', sshInfo().filePath ], {}, function (error) { - if (error) callback(new BoxError(BoxError.FS_ERROR, error)); - - callback(null, { enabled: result.trim() === 'true' }); - }); - cp.stdout.on('data', (data) => result = result + data.toString('utf8')); + return stdoutResult.trim() === 'true'; } -function enableRemoteSupport(enable, auditSource, callback) { +async function enableRemoteSupport(enable, auditSource) { assert.strictEqual(typeof enable, 'boolean'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const si = sshInfo(); - shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {}, function (error) { - if (error) callback(new BoxError(BoxError.FS_ERROR, error)); + const [error] = await safe(shell.promises.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {})); + if (error) throw new BoxError(BoxError.FS_ERROR, error); - eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable }); - - callback(); - }); + await eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable }); } diff --git a/src/test/updatechecker-test.js b/src/test/updatechecker-test.js index 3e71f7445..633c1c505 100644 --- a/src/test/updatechecker-test.js +++ b/src/test/updatechecker-test.js @@ -30,54 +30,45 @@ describe('updatechecker', function () { settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER, done); }); - it('no updates', function (done) { + it('no updates', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/boxupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false }) .reply(204, { } ); - updatechecker.checkForUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box).to.not.be.ok(); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker.checkForUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo().box).to.not.be.ok(); + expect(scope.isDone()).to.be.ok(); }); - it('new version', function (done) { + it('new version', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/boxupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false }) .reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } ); - updatechecker.checkForUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); - expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker.checkForUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); + expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); + expect(scope.isDone()).to.be.ok(); }); - it('bad response offers whatever was last valid', function (done) { + it('bad response offers whatever was last valid', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/boxupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, automatic: false }) .reply(404, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: 'box-pre.tar.gz' } ); - updatechecker.checkForUpdates({ automatic: false }, function (error) { - expect(error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); - expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker.checkForUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); + expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); + expect(scope.isDone()).to.be.ok(); }); }); @@ -88,62 +79,50 @@ describe('updatechecker', function () { settings.setAutoupdatePattern(constants.AUTOUPDATE_PATTERN_NEVER, done); }); - it('no updates', function (done) { + it('no updates', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/appupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false }) .reply(204, { } ); - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo()).to.eql({}); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker._checkAppUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo()).to.eql({}); + expect(scope.isDone()).to.be.ok(); }); - it('bad response', function (done) { + it('bad response', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/appupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false }) .reply(500, { update: { manifest: { version: '1.0.0', changelog: '* some changes' } } } ); - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo()).to.eql({}); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker._checkAppUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo()).to.eql({}); + expect(scope.isDone()).to.be.ok(); }); - it('offers new version', function (done) { + it('offers new version', async function () { nock.cleanAll(); - var scope = nock(mockApiServerOrigin) + const scope = nock(mockApiServerOrigin) .get('/api/v1/appupdate') .query({ boxVersion: constants.VERSION, accessToken: appstoreToken, appId: app.appStoreId, appVersion: app.manifest.version, automatic: false }) .reply(200, { manifest: { version: '2.0.0', changelog: '* some changes' } } ); - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo()).to.eql({ 'appid': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } }); - expect(scope.isDone()).to.be.ok(); - done(); - }); + await updatechecker._checkAppUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo()).to.eql({ 'appid': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } }); + expect(scope.isDone()).to.be.ok(); }); - it('does not offer old version', function (done) { + it('does not offer old version', async function () { nock.cleanAll(); - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo()).to.eql({ }); - done(); - }); + await updatechecker._checkAppUpdates({ automatic: false }); + expect(updatechecker.getUpdateInfo()).to.eql({ }); }); }); }); diff --git a/src/updatechecker.js b/src/updatechecker.js index 50900fe2a..dc825edcf 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -11,11 +11,11 @@ exports = module.exports = { const apps = require('./apps.js'), appstore = require('./appstore.js'), assert = require('assert'), - async = require('async'), debug = require('debug')('box:updatechecker'), notifications = require('./notifications.js'), paths = require('./paths.js'), - safe = require('safetydance'); + safe = require('safetydance'), + util = require('util'); function setUpdateInfo(state) { // appid -> update info { creationDate, manifest } @@ -31,95 +31,75 @@ function getUpdateInfo() { return state; } -function checkAppUpdates(options, callback) { +async function checkAppUpdates(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); debug('checkAppUpdates: checking for updates'); let state = getUpdateInfo(); let newState = { }; // create new state so that old app ids are removed - apps.getAll(function (error, result) { - if (error) return callback(error); + const appsGetAllAsync = util.promisify(apps.getAll); - async.eachSeries(result, function (app, iteratorDone) { - if (app.appStoreId === '') return iteratorDone(); // appStoreId can be '' for dev apps + const result = await appsGetAllAsync(); - appstore.getAppUpdate(app, options, function (error, updateInfo) { - if (error) { - debug('checkAppUpdates: Error getting app update info for %s', app.id, error); - return iteratorDone(); // continue to next - } + for (const app of result) { + if (app.appStoreId === '') continue; // appStoreId can be '' for dev apps - if (!updateInfo) return iteratorDone(); // skip if no next version is found + const [error, updateInfo] = await safe(appstore.getAppUpdate(app, options)); + if (error) { + debug('checkAppUpdates: Error getting app update info for %s', app.id, error); + continue; // continue to next + } - newState[app.id] = updateInfo; + if (!updateInfo) continue; // skip if no next version is found + newState[app.id] = updateInfo; + } - iteratorDone(); - }); - }, function () { - if ('box' in state) newState.box = state.box; // preserve the latest box state information - - setUpdateInfo(newState); - - callback(); - }); - }); + if ('box' in state) newState.box = state.box; // preserve the latest box state information + setUpdateInfo(newState); } -function checkBoxUpdates(options, callback) { +async function checkBoxUpdates(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); debug('checkBoxUpdates: checking for updates'); - appstore.getBoxUpdate(options, async function (error, updateInfo) { - if (error) return callback(error); + const updateInfo = await appstore.getBoxUpdate(options); - let state = getUpdateInfo(); + let state = getUpdateInfo(); - if (!updateInfo) { // no update - if ('box' in state) { - delete state.box; - setUpdateInfo(state); - } - debug('checkBoxUpdates: no updates'); - return callback(null); + if (!updateInfo) { // no update + if ('box' in state) { + delete state.box; + setUpdateInfo(state); } + debug('checkBoxUpdates: no updates'); + return; + } - if (state.box && state.box.version === updateInfo.version) { - debug(`checkBoxUpdates: Skipping notification of box update ${updateInfo.version} as user was already notified`); - return callback(null); - } + if (state.box && state.box.version === updateInfo.version) { + debug(`checkBoxUpdates: Skipping notification of box update ${updateInfo.version} as user was already notified`); + return; + } - debug(`checkBoxUpdates: ${updateInfo.version} is available`); + debug(`checkBoxUpdates: ${updateInfo.version} is available`); - const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join(''); + const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join(''); + const message = `Changelog:\n${changelog}\n\nGo to the settings view to update.\n\n`; - const message = `Changelog:\n${changelog}\n\nGo to the settings view to update.\n\n`; + await notifications.alert(notifications.ALERT_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message); - [error] = await safe(notifications.alert(notifications.ALERT_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message)); - if (error) return callback(error); - - state.box = updateInfo; - setUpdateInfo(state); - - callback(null); - }); + state.box = updateInfo; + setUpdateInfo(state); } -function checkForUpdates(options, callback) { +async function checkForUpdates(options) { assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - checkBoxUpdates(options, function (boxError) { - if (boxError) debug('checkForUpdates: error checking for box updates:', boxError); + const [boxError] = await safe(checkBoxUpdates(options)); + if (boxError) debug('checkForUpdates: error checking for box updates:', boxError); - checkAppUpdates(options, function (appError) { - if (appError) debug('checkForUpdates: error checking for app updates:', appError); - - callback(boxError || appError || null); - }); - }); + const [appError] = await safe(checkAppUpdates(options)); + if (appError) debug('checkForUpdates: error checking for app updates:', appError); }