Files
cloudron-box/src/appdb.js

587 lines
26 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
get: get,
2015-09-14 17:01:04 -07:00
getByContainerId: getByContainerId,
add: add,
exists: exists,
del: del,
update: update,
getAll: getAll,
getPortBindings: getPortBindings,
2017-10-23 22:05:43 +02:00
delPortBinding: delPortBinding,
setAddonConfig: setAddonConfig,
getAddonConfig: getAddonConfig,
getAddonConfigByAppId: getAddonConfigByAppId,
2017-03-26 19:06:36 -07:00
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setTask: setTask,
getAppStoreIds: getAppStoreIds,
2018-06-29 11:04:14 +02:00
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
_clear: clear
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance'),
util = require('util');
2019-08-30 09:45:43 -07:00
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'subdomains.subdomain AS location', 'subdomains.domain',
2020-01-28 21:30:35 -08:00
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
2020-11-10 09:59:28 -08:00
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth',
2019-11-14 21:43:14 -08:00
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
2018-08-24 10:39:59 -07:00
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
assert(result.manifestJson === null || typeof result.manifestJson === 'string');
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
2019-03-22 07:48:31 -07:00
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
result.tags = safe.JSON.parse(result.tagsJson) || [];
2019-03-22 07:48:31 -07:00
delete result.tagsJson;
assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string');
result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {};
delete result.reverseProxyConfigJson;
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
2015-10-13 12:29:40 +02:00
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
2016-07-15 11:08:11 +02:00
2016-11-11 10:53:41 +05:30
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
2020-11-10 09:59:28 -08:00
result.proxyAuth = !!result.proxyAuth;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
2020-05-03 17:38:15 -07:00
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
let volumeIds = JSON.parse(result.volumeIds);
delete result.volumeIds;
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
delete result.volumeReadOnlys;
2020-10-29 21:58:14 -07:00
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
2019-09-23 09:13:43 -07:00
result.error = safe.JSON.parse(result.errorJson);
2019-08-30 09:45:43 -07:00
delete result.errorJson;
2019-08-26 15:28:29 -07:00
result.taskId = result.taskId ? String(result.taskId) : null;
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues,'
2020-10-28 19:42:48 -07:00
+ 'JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys '
2018-06-29 11:04:14 +02:00
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
2020-10-28 19:42:48 -07:00
+ ' LEFT OUTER JOIN appMounts ON apps.id = appMounts.appId'
2018-06-29 11:04:14 +02:00
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
2018-08-24 10:39:59 -07:00
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
2015-09-14 17:01:04 -07:00
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues,'
2020-10-28 19:42:48 -07:00
+ 'JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys '
2018-06-29 11:04:14 +02:00
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
2020-10-28 19:42:48 -07:00
+ ' LEFT OUTER JOIN appMounts ON apps.id = appMounts.appId'
2018-06-29 11:04:14 +02:00
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
2015-09-14 17:01:04 -07:00
2018-08-24 10:39:59 -07:00
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
2015-09-14 17:01:04 -07:00
callback(null, result[0]);
});
2015-09-14 17:01:04 -07:00
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues,'
2020-10-28 19:42:48 -07:00
+ 'JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys '
2018-06-29 11:04:14 +02:00
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
2020-10-28 19:42:48 -07:00
+ ' LEFT OUTER JOIN appMounts ON apps.id = appMounts.appId'
2018-06-29 11:04:14 +02:00
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2018-08-24 10:39:59 -07:00
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
});
}
2019-07-02 20:22:17 -07:00
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
2017-11-02 22:16:42 +01:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
2016-06-17 16:43:35 -05:00
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
var manifestJson = JSON.stringify(manifest);
2016-06-17 16:43:35 -05:00
2019-03-22 07:48:31 -07:00
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
2020-01-28 21:30:35 -08:00
const cpuShares = data.cpuShares || 512;
const installationState = data.installationState;
const runState = data.runState;
2019-03-22 07:48:31 -07:00
const sso = 'sso' in data ? data.sso : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
const env = data.env || {};
const label = data.label || null;
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
2019-03-22 14:26:25 -07:00
const mailboxName = data.mailboxName || null;
2019-11-14 21:43:14 -08:00
const mailboxDomain = data.mailboxDomain || null;
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
var queries = [];
2018-06-29 11:04:14 +02:00
queries.push({
2020-01-28 21:30:35 -08:00
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
2019-11-14 21:43:14 -08:00
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
2020-01-28 21:30:35 -08:00
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
2019-11-14 21:43:14 -08:00
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
});
2018-06-29 11:04:14 +02:00
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
Object.keys(env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
});
});
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function exists(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result.length !== 0);
});
}
function getPortBindings(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, type, callback) {
2017-10-23 22:05:43 +02:00
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
2017-10-23 22:05:43 +02:00
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
2017-10-23 22:05:43 +02:00
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
var queries = [
2018-06-29 11:04:14 +02:00
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
2020-01-31 15:28:42 -08:00
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
2020-10-28 19:42:48 -07:00
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
2018-06-29 11:04:14 +02:00
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
}
function update(id, app, callback) {
updateWithConstraints(id, app, '', callback);
}
function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof constraints, 'string');
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
2018-06-29 16:50:09 +02:00
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('tags' in app) || Array.isArray(app.tags));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
if ('portBindings' in app) {
var portBindings = app.portBindings || { };
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
});
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
2018-06-29 11:04:14 +02:00
if ('alternateDomains' in app) {
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
}
2020-10-28 19:42:48 -07:00
if ('mounts' in app) {
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
app.mounts.forEach(function (m) {
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
});
}
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
fields.push(`${p}Json = ?`);
2017-01-20 09:40:11 -08:00
values.push(JSON.stringify(app[p]));
2020-10-29 21:58:14 -07:00
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env' && p !== 'mounts') {
fields.push(p + ' = ?');
values.push(app[p]);
}
}
if (values.length !== 0) {
values.push(id);
queries.push({ query: 'UPDATE apps SET ' + fields.join(', ') + ' WHERE id = ? ' + constraints, args: values });
}
database.transaction(queries, function (error, results) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
return callback(null);
});
}
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health, healthTime };
updateWithConstraints(appId, values, '', callback);
}
2019-09-24 20:29:01 -07:00
function setTask(appId, values, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof values, 'object');
2019-09-24 20:29:01 -07:00
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
2019-09-24 20:29:01 -07:00
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
if (options.requiredState === null) {
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
} else {
2019-09-24 20:29:01 -07:00
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
}
}
function getAppStoreIds(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function setAddonConfig(appId, addonId, env, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert(util.isArray(env));
assert.strictEqual(typeof callback, 'function');
unsetAddonConfig(appId, addonId, function (error) {
if (error) return callback(error);
if (env.length === 0) return callback(null);
var query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES ';
var args = [ ], queryArgs = [ ];
for (var i = 0; i < env.length; i++) {
args.push(appId, addonId, env[i].name, env[i].value);
queryArgs.push('(?, ?, ?, ?)');
}
database.query(query + queryArgs.join(','), args, function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
});
}
function unsetAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function unsetAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function getAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
2017-03-26 19:06:36 -07:00
2019-06-17 11:13:59 -07:00
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
assert.strictEqual(typeof addonId, 'string');
2019-06-17 11:13:59 -07:00
assert.strictEqual(typeof namePattern, 'string');
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
2019-06-17 11:13:59 -07:00
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, namePattern, callback) {
2017-03-26 19:06:36 -07:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
2017-03-26 19:06:36 -07:00
assert.strictEqual(typeof callback, 'function');
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-10-24 20:48:38 -07:00
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
2017-03-26 19:06:36 -07:00
callback(null, results[0].value);
});
}