diff --git a/src/appstore.js b/src/appstore.js index d7d752cf5..582923585 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -491,7 +491,7 @@ function createTicket(info, auditSource, callback) { eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info); - callback(null); + callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` }); }); }); }); diff --git a/src/mailer.js b/src/mailer.js index 7b08ad837..e850572ea 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -26,7 +26,6 @@ exports = module.exports = { var assert = require('assert'), BoxError = require('./boxerror.js'), - constants = require('./constants.js'), debug = require('debug')('box:mailer'), ejs = require('ejs'), mail = require('./mail.js'), @@ -47,15 +46,16 @@ function getMailConfig(callback) { assert.strictEqual(typeof callback, 'function'); settings.getCloudronName(function (error, cloudronName) { - // this is not fatal - if (error) { - debug(error); - cloudronName = 'Cloudron'; - } + if (error) debug('Error getting cloudron name: ', error); - callback(null, { - cloudronName: cloudronName, - notificationFrom: `"${cloudronName}" ` + settings.getSupportConfig(function (error, supportConfig) { + if (error) debug('Error getting support config: ', error); + + callback(null, { + cloudronName: cloudronName || '', + notificationFrom: `"${cloudronName}" `, + supportEmail: supportConfig.email + }); }); }); } @@ -280,7 +280,7 @@ function appDied(mailTo, app) { from: mailConfig.notificationFrom, to: mailTo, subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn), - text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: constants.SUPPORT_EMAIL, format: 'text' }) + text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: mailConfig.supportEmail, format: 'text' }) }; sendMail(mailOptions); diff --git a/src/routes/settings.js b/src/routes/settings.js index 9949ab68b..bc56e63e9 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -117,6 +117,14 @@ function setFooter(req, res, next) { }); } +function getSupportConfig(req, res, next) { + settings.getSupportConfig(function (error, supportConfig) { + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, supportConfig)); + }); +} + function setCloudronAvatar(req, res, next) { assert.strictEqual(typeof req.files, 'object'); @@ -330,6 +338,8 @@ function get(req, res, next) { case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next); + case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next); + default: return next(new HttpError(404, 'No such setting')); } } diff --git a/src/routes/support.js b/src/routes/support.js index f4c03f127..15775b8c7 100644 --- a/src/routes/support.js +++ b/src/routes/support.js @@ -4,7 +4,10 @@ exports = module.exports = { createTicket: createTicket, getRemoteSupport: getRemoteSupport, - enableRemoteSupport: enableRemoteSupport + enableRemoteSupport: enableRemoteSupport, + + canCreateTicket: canCreateTicket, + canEnableRemoteSupport: canEnableRemoteSupport }; var appstore = require('../appstore.js'), @@ -13,9 +16,20 @@ var appstore = require('../appstore.js'), constants = require('../constants.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + 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)); + + if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin')); + + next(); + }); +} + function createTicket(req, res, next) { assert.strictEqual(typeof req.user, 'object'); @@ -28,10 +42,25 @@ function createTicket(req, res, next) { if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string')); if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string')); - appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error) { - if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`)); + 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')); - next(new HttpSuccess(201, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` })); + 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}`)); + + next(new HttpSuccess(201, result)); + }); + }); +} + +function canEnableRemoteSupport(req, res, next) { + settings.getSupportConfig(function (error, supportConfig) { + if (error) return next(new HttpError(503, error.message)); + + if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin')); + + next(); }); } diff --git a/src/server.js b/src/server.js index ca633d7ee..efd6dfdfd 100644 --- a/src/server.js +++ b/src/server.js @@ -281,9 +281,9 @@ function initializeExpressSync() { router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList); // support - router.post('/api/v1/support/ticket', cloudronScope, routes.support.createTicket); + router.post('/api/v1/support/ticket', cloudronScope, routes.support.canCreateTicket, routes.support.createTicket); router.get ('/api/v1/support/remote_support', cloudronScope, routes.support.getRemoteSupport); - router.post('/api/v1/support/remote_support', cloudronScope, routes.support.enableRemoteSupport); + router.post('/api/v1/support/remote_support', cloudronScope, routes.support.canEnableRemoteSupport, routes.support.enableRemoteSupport); // domain routes router.post('/api/v1/domains', domainsManageScope, routes.domains.add); diff --git a/src/settings.js b/src/settings.js index 085bd5d4b..b06ef25f8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -50,6 +50,7 @@ exports = module.exports = { setFooter: setFooter, getAppstoreListingConfig: getAppstoreListingConfig, + getSupportConfig: getSupportConfig, provider: provider, @@ -81,6 +82,7 @@ exports = module.exports = { REGISTRY_CONFIG_KEY: 'registry_config', SYSINFO_CONFIG_KEY: 'sysinfo_config', APPSTORE_LISTING_CONFIG_KEY: 'appstore_listing_config', + SUPPORT_CONFIG_KEY: 'support_config', // strings APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern', @@ -163,6 +165,17 @@ let gDefaults = (function () { whitelist: null // null imples, not set. this is an object and not an array }; + result[exports.SUPPORT_CONFIG_KEY] = { + email: 'support@cloudron.io', + remoteSupport: true, + ticketFormBody: + 'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n' + + '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n' + + '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n' + + '* [Forum](https://forum.cloudron.io/)\n\n', + submitTickets: true + }; + result[exports.FOOTER_KEY] = '© 2020 [Cloudron](https://cloudron.io) [Forum ](https://forum.cloudron.io)'; return result; @@ -539,6 +552,17 @@ function getAppstoreListingConfig(callback) { }); } +function getSupportConfig(callback) { + assert.strictEqual(typeof callback, 'function'); + + 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)); + }); +} + function getLicenseKey(callback) { assert.strictEqual(typeof callback, 'function');