Просмотр исходного кода

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

Redundant subscription notification if entitlement via SSO

GitOrigin-RevId: 8529204e78c3a43d87acbb375fea15c62cad48a3
Eric Mc Sween 6 лет назад
Родитель
Сommit
00bdc52fab

+ 171 - 152
services/web/app/src/Features/Notifications/NotificationsBuilder.js

@@ -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() {}
+      }
+      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)
     }
-  },
+  }
+}
 
-  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 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)
+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)
     }
-  },
-
-  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
-            )
+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
           }
-        )
-      },
 
-      read(university_id, callback) {
-        if (callback == null) {
-          callback = function() {}
+          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
+          )
         }
-        const key = `ip-matched-affiliation-${university_id}`
-        return NotificationsHandler.markAsReadWithKey(userId, key, 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)
+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

+ 26 - 0
services/web/app/src/Features/User/SAMLIdentityManager.js

@@ -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

+ 55 - 0
services/web/test/unit/src/User/SAMLIdentityManagerTests.js

@@ -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
+      })
+    })
+  })
 })