diff --git a/src/appstore.js b/src/appstore.js index b01d0c814..ede3ae181 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -13,6 +13,8 @@ exports = module.exports = { getAccount: getAccount, + sendFeedback: sendFeedback, + AppstoreError: AppstoreError }; @@ -267,3 +269,26 @@ function getAccount(callback) { }); }); } + +function sendFeedback(info, callback) { + assert.strictEqual(typeof info, 'object'); + assert.strictEqual(typeof info.email, 'string'); + assert.strictEqual(typeof info.displayName, 'string'); + assert.strictEqual(typeof info.type, 'string'); + assert.strictEqual(typeof info.subject, 'string'); + assert.strictEqual(typeof info.description, 'string'); + assert.strictEqual(typeof callback, 'function'); + + getAppstoreConfig(function (error, appstoreConfig) { + if (error) return callback(error); + + var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback'; + + superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) { + if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error)); + if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text))); + + callback(null); + }); + }); +} diff --git a/src/mail_templates/feedback.ejs b/src/mail_templates/feedback.ejs deleted file mode 100644 index 901b9dd50..000000000 --- a/src/mail_templates/feedback.ejs +++ /dev/null @@ -1,14 +0,0 @@ -<%if (format === 'text') { %> - -New <%= type %> from <%= fqdn %>. - -Sender: <%= user.email %> -Sent at: <%= new Date().toUTCString() %> - -Subject: <%= subject %> ------------------------------------------------------------ -<%= description %> - -<% } else { %> - -<% } %> diff --git a/src/mailer.js b/src/mailer.js index 2881db4c7..fc37c2abe 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -23,22 +23,13 @@ exports = module.exports = { certificateRenewalError: certificateRenewalError, - FEEDBACK_TYPE_FEEDBACK: 'feedback', - FEEDBACK_TYPE_TICKET: 'ticket', - FEEDBACK_TYPE_APP_MISSING: 'app_missing', - FEEDBACK_TYPE_APP_ERROR: 'app_error', - FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request', - sendFeedback: sendFeedback, - sendTestMail: sendTestMail, _getMailQueue: _getMailQueue, _clearMailQueue: _clearMailQueue }; -var appstore = require('./appstore.js'), - AppstoreError = appstore.AppstoreError, - assert = require('assert'), +var assert = require('assert'), async = require('async'), config = require('./config.js'), debug = require('debug')('box:mailer'), @@ -575,28 +566,6 @@ function unexpectedExit(program, context, callback) { sendMails([ mailOptions ], callback); } -function sendFeedback(user, type, subject, description) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof subject, 'string'); - assert.strictEqual(typeof description, 'string'); - - assert(type === exports.FEEDBACK_TYPE_TICKET || - type === exports.FEEDBACK_TYPE_FEEDBACK || - type === exports.FEEDBACK_TYPE_APP_MISSING || - type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST || - type === exports.FEEDBACK_TYPE_APP_ERROR); - - var mailOptions = { - from: mailConfig().from, - to: 'support@cloudron.io', - subject: util.format('[%s] %s - %s', type, config.fqdn(), subject), - text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'}) - }; - - enqueue(mailOptions); -} - function sendTestMail(email) { assert.strictEqual(typeof email, 'string'); diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 23a04d035..3598b3902 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -19,7 +19,8 @@ exports = module.exports = { sendTestMail: sendTestMail }; -var assert = require('assert'), +var appstore = require('../appstore.js'), + assert = require('assert'), async = require('async'), cloudron = require('../cloudron.js'), CloudronError = cloudron.CloudronError, @@ -224,17 +225,19 @@ function checkForUpdates(req, res, next) { function feedback(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && - req.body.type !== mailer.FEEDBACK_TYPE_TICKET && - req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING && - req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST && - req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"')); + const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ]; + + if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string')); + if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type')); if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string')); if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string')); - mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description); + appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) { + if (error) return next(new HttpError(503, error.message)); + + next(new HttpSuccess(201, {})); + }); - next(new HttpSuccess(201, {})); } function getLogs(req, res, next) { diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index d19699a61..f08acbdd0 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -540,26 +540,6 @@ describe('Cloudron', function () { }); }); - it('succeeds with ticket type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - - it('succeeds with app type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - it('fails without description', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') .send({ type: 'ticket', subject: 'some subject' }) @@ -590,16 +570,6 @@ describe('Cloudron', function () { }); }); - it('succeeds with feedback type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'feedback', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - it('fails without subject', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') .send({ type: 'ticket', description: 'some description' }) @@ -609,6 +579,42 @@ describe('Cloudron', function () { done(); }); }); + + it('succeeds with ticket type', function (done) { + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }); + var scope2 = nock(config.apiServerOrigin()) + .filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body + .post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN') + .reply(201, { }); + + superagent.post(SERVER_URL + '/api/v1/feedback') + .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + done(); + }); + }); + + it('succeeds with app type', function (done) { + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }); + var scope2 = nock(config.apiServerOrigin()) + .filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body + .post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN') + .reply(201, { }); + + superagent.post(SERVER_URL + '/api/v1/feedback') + .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + done(); + }); + }); }); describe('logs', function () {