diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index f4cfaa5272..06ba7afd75 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -88,5 +88,7 @@ "questNotPending": "There is no quest to start.", "questOrGroupLeaderOnlyStartQuest": "Only the quest leader or group leader can force start the quest", "noAdminAccess": "You don't have admin access.", - "pageMustBeNumber": "req.query.page must be a number" + "pageMustBeNumber": "req.query.page must be a number", + "missingUnsubscriptionCode": "Missing unsubscription code.", + "userNotFound": "User Not Found" } diff --git a/test/api/v3/integration/unsubscription/POST-paymentId-subscribe-cancel.test.js b/test/api/v3/integration/unsubscription/POST-paymentId-subscribe-cancel.test.js new file mode 100644 index 0000000000..359c0061df --- /dev/null +++ b/test/api/v3/integration/unsubscription/POST-paymentId-subscribe-cancel.test.js @@ -0,0 +1,68 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { encrypt } from '../../../../../website/src/libs/api-v3/encryption'; +import { v4 as generateUUID } from 'uuid'; + +describe('GET /unsubscribe', () => { + let user; + let testEmail = 'test@habitica.com'; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('return error when code is not provided', async () => { + await expect(user.get('/unsubscribe')).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Invalid request parameters.', + }); + }); + + it('return error when user is not found', async () => { + let code = encrypt(JSON.stringify({ + _id: generateUUID(), + })); + + await expect(user.get(`/unsubscribe?code=${code}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userNotFound'), + }); + }); + + it('unsubscribes a user from email notifications', async () => { + let code = encrypt(JSON.stringify({ + _id: user._id, + email: user.email, + })); + + await user.get(`/unsubscribe?code=${code}`); + + let unsubscribedUser = await user.get('/user'); + + expect(unsubscribedUser.preferences.emailNotifications.unsubscribeFromAll).to.be.true; + }); + + it('unsubscribes an email from notifications', async () => { + let code = encrypt(JSON.stringify({ + email: testEmail, + })); + + let unsubscribedMessage = await user.get(`/unsubscribe?code=${code}`); + + expect(unsubscribedMessage).to.equal('

Unsubscribed successfully!

You won\'t receive any other email from Habitica.'); + }); + + it('returns okay when email is already unsubscribed', async () => { + let code = encrypt(JSON.stringify({ + email: testEmail, + })); + + let unsubscribedMessage = await user.get(`/unsubscribe?code=${code}`); + + expect(unsubscribedMessage).to.equal('

Unsubscribed successfully!

You won\'t receive any other email from Habitica.'); + }); +}); diff --git a/website/src/controllers/api-v3/unsubscription.js b/website/src/controllers/api-v3/unsubscription.js new file mode 100644 index 0000000000..5578cd742f --- /dev/null +++ b/website/src/controllers/api-v3/unsubscription.js @@ -0,0 +1,57 @@ +import { model as User } from '../../models/user'; +import { model as EmailUnsubscription } from '../../models/emailUnsubscription'; +import { decrypt } from '../../libs/api-v3/encryption'; +import { + NotFound, +} from '../../libs/api-v3/errors'; + +let api = {}; + +/** + * @api {post} /unsubscribe Unsubscribe an email or user from email notifications + * @apiVersion 3.0.0 + * @apiName UnsubscribeEmail + * @apiGroup Unsubscribe + * + * @apiParam {String} code An unsubscription code + * + * @apiSuccess {String} okRes An message stating the user/email unsubscribed successfully + */ +api.unsubscribe = { + method: 'GET', + url: '/unsubscribe', + middlewares: [], + async handler (req, res) { + req.checkQuery({ + code: { + notEmpty: {errorMessage: res.t('missingUnsubscriptionCode')}, + }, + }); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let data = JSON.parse(decrypt(req.query.code)); + + if (data._id) { + let userUpdated = await User.update( + {_id: data._id}, + { $set: {'preferences.emailNotifications.unsubscribeFromAll': true}} + ); + + if (userUpdated.nModified !== 1) throw new NotFound(res.t('userNotFound')); + + res.send(`

${res.t('unsubscribedSuccessfully', null, req.language)}

res.t('unsubscribedTextUsers', null, req.language)`); + } else { + let unsubscribedEmail = await EmailUnsubscription.findOne({email: data.email}); + let okResponse = `

${res.t('unsubscribedSuccessfully', null, req.language)}

${res.t('unsubscribedTextOthers', null, req.language)}`; + if (unsubscribedEmail) return res.send(okResponse); + + await EmailUnsubscription.create({email: data.email}); + + res.send(okResponse); + } + }, +}; + +export default api;