Merge pull request #2840 from overleaf/jel-sso-redundant-subscription-notification

Redundant subscription notification if entitlement via SSO

GitOrigin-RevId: 8529204e78c3a43d87acbb375fea15c62cad48a3
This commit is contained in:
Eric Mc Sween 2020-05-20 10:21:13 -04:00 committed by Copybot
parent a0809e1d85
commit 00bdc52fab
3 changed files with 266 additions and 166 deletions

View file

@ -11,177 +11,196 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const NotificationsHandler = require('./NotificationsHandler')
const { promisifyAll } = require('../../util/promises')
const request = require('request')
const settings = require('settings-sharelatex')
module.exports = {
// Note: notification keys should be url-safe
featuresUpgradedByAffiliation(affiliation, user) {
return {
key: `features-updated-by=${affiliation.institutionId}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_features_upgraded_by_affiliation',
messageOpts,
null,
false,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
function featuresUpgradedByAffiliation(affiliation, user) {
return {
key: `features-updated-by=${affiliation.institutionId}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
}
},
redundantPersonalSubscription(affiliation, user) {
return {
key: `redundant-personal-subscription-${affiliation.institutionId}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_personal_subscription_not_required_due_to_affiliation',
messageOpts,
null,
false,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
},
projectInvite(invite, project, sendingUser, user) {
return {
key: `project-invite-${invite._id}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = {
userName: sendingUser.first_name,
projectName: project.name,
projectId: project._id.toString(),
token: invite.token
}
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_project_invite',
messageOpts,
invite.expires,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
},
ipMatcherAffiliation(userId) {
return {
create(ip, callback) {
if (callback == null) {
callback = function() {}
}
if (!settings.apis.v1.url) {
return null
} // service is not configured
return request(
{
method: 'GET',
url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`,
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass },
body: { ip },
json: true,
timeout: 20 * 1000
},
function(error, response, body) {
if (error != null) {
return error
}
if (response.statusCode !== 200) {
return null
}
const key = `ip-matched-affiliation-${body.id}`
const messageOpts = {
university_name: body.name,
content: body.enrolment_ad_html
}
return NotificationsHandler.createNotification(
userId,
key,
'notification_ip_matched_affiliation',
messageOpts,
null,
false,
callback
)
}
)
},
read(university_id, callback) {
if (callback == null) {
callback = function() {}
}
const key = `ip-matched-affiliation-${university_id}`
return NotificationsHandler.markAsReadWithKey(userId, key, callback)
}
}
},
tpdsFileLimit(user_id) {
return {
key: `tpdsFileLimit-${user_id}`,
create(projectName, callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = {
projectName: projectName
}
return NotificationsHandler.createNotification(
user_id,
this.key,
'notification_tpds_file_limit',
messageOpts,
null,
true,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_features_upgraded_by_affiliation',
messageOpts,
null,
false,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
function redundantPersonalSubscription(affiliation, user) {
return {
key: `redundant-personal-subscription-${affiliation.institutionId}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = { institutionName: affiliation.institutionName }
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_personal_subscription_not_required_due_to_affiliation',
messageOpts,
null,
false,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
function projectInvite(invite, project, sendingUser, user) {
return {
key: `project-invite-${invite._id}`,
create(callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = {
userName: sendingUser.first_name,
projectName: project.name,
projectId: project._id.toString(),
token: invite.token
}
return NotificationsHandler.createNotification(
user._id,
this.key,
'notification_project_invite',
messageOpts,
invite.expires,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
function ipMatcherAffiliation(userId) {
return {
create(ip, callback) {
if (callback == null) {
callback = function() {}
}
if (!settings.apis.v1.url) {
return null
} // service is not configured
return request(
{
method: 'GET',
url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`,
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass },
body: { ip },
json: true,
timeout: 20 * 1000
},
function(error, response, body) {
if (error != null) {
return error
}
if (response.statusCode !== 200) {
return null
}
const key = `ip-matched-affiliation-${body.id}`
const messageOpts = {
university_name: body.name,
content: body.enrolment_ad_html
}
return NotificationsHandler.createNotification(
userId,
key,
'notification_ip_matched_affiliation',
messageOpts,
null,
false,
callback
)
}
)
},
read(university_id, callback) {
if (callback == null) {
callback = function() {}
}
const key = `ip-matched-affiliation-${university_id}`
return NotificationsHandler.markAsReadWithKey(userId, key, callback)
}
}
}
function tpdsFileLimit(user_id) {
return {
key: `tpdsFileLimit-${user_id}`,
create(projectName, callback) {
if (callback == null) {
callback = function() {}
}
const messageOpts = {
projectName: projectName
}
return NotificationsHandler.createNotification(
user_id,
this.key,
'notification_tpds_file_limit',
messageOpts,
null,
true,
callback
)
},
read(callback) {
if (callback == null) {
callback = function() {}
}
return NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
}
}
}
const NotificationsBuilder = {
// Note: notification keys should be url-safe
featuresUpgradedByAffiliation,
redundantPersonalSubscription,
projectInvite,
ipMatcherAffiliation,
tpdsFileLimit
}
NotificationsBuilder.promises = {
redundantPersonalSubscription: function(affiliation, user) {
return promisifyAll(redundantPersonalSubscription(affiliation, user))
}
}
module.exports = NotificationsBuilder

View file

@ -1,7 +1,9 @@
const EmailHandler = require('../Email/EmailHandler')
const Errors = require('../Errors/Errors')
const InstitutionsAPI = require('../Institutions/InstitutionsAPI')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const OError = require('@overleaf/o-error')
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
const UserGetter = require('../User/UserGetter')
const UserUpdater = require('../User/UserUpdater')
const logger = require('logger-sharelatex')
@ -151,6 +153,24 @@ async function getUser(providerId, externalUserId) {
return user
}
async function redundantSubscription(userId, providerId, providerName) {
const subscription = await SubscriptionLocator.promises.getUserIndividualSubscription(
userId
)
if (subscription) {
await NotificationsBuilder.promises
.redundantPersonalSubscription(
{
institutionId: providerId,
institutionName: providerName
},
{ _id: userId }
)
.create()
}
}
async function linkAccounts(
userId,
externalUserId,
@ -171,6 +191,11 @@ async function linkAccounts(
// update v1 affiliations record
if (hasEntitlement) {
await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail)
try {
await redundantSubscription(userId, providerId, providerName)
} catch (error) {
logger.err({ err: error }, 'error checking redundant subscription')
}
} else {
await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail)
}
@ -271,6 +296,7 @@ const SAMLIdentityManager = {
entitlementAttributeMatches,
getUser,
linkAccounts,
redundantSubscription,
unlinkAccounts,
updateEntitlement,
userHasEntitlement

View file

@ -49,6 +49,18 @@ describe('SAMLIdentityManager', function() {
sendEmail: sinon.stub().yields()
}),
'../Errors/Errors': this.Errors,
'../Notifications/NotificationsBuilder': (this.NotificationsBuilder = {
promises: {
redundantPersonalSubscription: sinon
.stub()
.returns({ create: sinon.stub().resolves() })
}
}),
'../Subscription/SubscriptionLocator': (this.SubscriptionLocator = {
promises: {
getUserIndividualSubscription: sinon.stub().resolves()
}
}),
'../../models/User': {
User: (this.User = {
findOneAndUpdate: sinon.stub(),
@ -212,4 +224,47 @@ describe('SAMLIdentityManager', function() {
).should.equal(false)
})
})
describe('redundantSubscription', function() {
const userId = '1bv'
const providerId = 123
const providerName = 'University Name'
describe('with a personal subscription', function() {
beforeEach(function() {
this.SubscriptionLocator.promises.getUserIndividualSubscription.resolves(
{
planCode: 'professional'
}
)
})
it('should create redundant personal subscription notification ', async function() {
try {
await this.SAMLIdentityManager.redundantSubscription(
userId,
providerId,
providerName
)
} catch (error) {
expect(error).to.not.exist
}
expect(this.NotificationsBuilder.promises.redundantPersonalSubscription)
.to.have.been.calledOnce
})
})
describe('without a personal subscription', function() {
it('should create redundant personal subscription notification ', async function() {
try {
await this.SAMLIdentityManager.redundantSubscription(
userId,
providerId,
providerName
)
} catch (error) {
expect(error).to.not.exist
}
expect(this.NotificationsBuilder.promises.redundantPersonalSubscription)
.to.not.have.been.called
})
})
})
})