diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 8a2c33536a..f8d90756b2 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -11,6 +11,7 @@ UserHandler = require("../User/UserHandler") UserSessionsManager = require("../User/UserSessionsManager") Analytics = require "../Analytics/AnalyticsManager" passport = require 'passport' +NotificationsBuilder = require("../Notifications/NotificationsBuilder") module.exports = AuthenticationController = @@ -112,6 +113,7 @@ module.exports = AuthenticationController = UserHandler.setupLoginData(user, ()->) LoginRateLimiter.recordSuccessfulLogin(user.email) AuthenticationController._recordSuccessfulLogin(user._id) + AuthenticationController.ipMatchCheck(req, user) Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip}) Analytics.identifyUser(user._id, req.sessionID) logger.log email: user.email, user_id: user._id.toString(), "successful log in" @@ -119,6 +121,13 @@ module.exports = AuthenticationController = # capture the request ip for use when creating the session user._login_req_ip = req.ip + ipMatchCheck: (req, user) -> + if req.ip != user.lastLoginIp + NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create() + UserUpdater.updateUser user._id.toString(), { + $set: { "lastLoginIp": req.ip } + } + setInSessionUser: (req, props) -> for key, value of props if req?.session?.passport?.user? diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 941f4d4d4d..c0280450ab 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -1,5 +1,7 @@ logger = require("logger-sharelatex") NotificationsHandler = require("./NotificationsHandler") +request = require "request" +settings = require "settings-sharelatex" module.exports = @@ -29,3 +31,29 @@ module.exports = NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback read: (callback=()->) -> NotificationsHandler.markAsReadByKeyOnly @key, callback + + ipMatcherAffiliation: (userId, ip) -> + key: "ip-matched-affiliation-#{ip}" + create: (callback=()->) -> + return null unless settings?.apis?.v1?.url # service is not configured + _key = @key + 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: ip } + json: true + timeout: 20 * 1000 + }, (error, response, body) -> + return error if error? + return null unless response.statusCode == 200 + + messageOpts = + university_id: body.id + university_name: body.name + content: body.enrolment_ad_html + logger.log user_id:userId, key:_key, "creating notification key for user" + NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, false, callback + + read: (callback = ->)-> + NotificationsHandler.markAsReadWithKey userId, @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index 5a6ca47c2e..a0f6ae5c12 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -29,12 +29,15 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)-> + if !callback + callback = forceCreate + forceCreate = true payload = { key:key messageOpts:messageOpts templateKey:templateKey - forceCreate: true + forceCreate:forceCreate } if expiryDateTime? payload.expires = expiryDateTime diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 24dca35e96..59c0647c19 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -26,6 +26,8 @@ TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler' Modules = require '../../infrastructure/Modules' ProjectEntityHandler = require './ProjectEntityHandler' +UserGetter = require("../User/UserGetter") +NotificationsBuilder = require("../Notifications/NotificationsBuilder") crypto = require 'crypto' { V1ConnectionError } = require '../Errors/Errors' Features = require('../../infrastructure/Features') @@ -209,6 +211,11 @@ module.exports = ProjectController = user = results.user warnings = ProjectController._buildWarningsList results.v1Projects + # in v2 add notifications for matching university IPs + if Settings.overleaf? + UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) -> + if req.ip != user.lastLoginIp + NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create() ProjectController._injectProjectOwners projects, (error, projects) -> return next(error) if error? diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 95bedebbf4..23b59375f4 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -22,6 +22,7 @@ UserSchema = new Schema confirmed : {type : Boolean, default : false} signUpDate : {type : Date, default: () -> new Date() } lastLoggedIn : {type : Date} + lastLoginIp : {type : String, default : ''} loginCount : {type : Number, default: 0} holdingAccount : {type : Boolean, default: false} ace : { diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug index 55798d6a2b..04ba3827dc 100644 --- a/services/web/app/views/project/list/notifications.pug +++ b/services/web/app/views/project/list/notifications.pug @@ -60,6 +60,21 @@ span(ng-controller="NotificationsController").userNotifications button(ng-click="dismiss(notification)").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} + .alert.alert-info(ng-switch-when="notification_ip_matched_affiliation") + div.notification_inner + .notification_body + | It looks like you're at + strong {{ notification.messageOpts.university_name }}!
+ | Did you know that {{notification.messageOpts.university_name}} is providing + strong free Overleaf Professional accounts + | to everyone at {{notification.messageOpts.university_name}}?
+ | Add an institutional email address to claim your account. + a.pull-right.btn.btn-sm.btn-info(href="/user/settings") + | Add Affiliation + span().notification_close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} .alert.alert-info(ng-switch-default) div.notification_inner span(ng-bind-html="notification.html").notification_body diff --git a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee index 24af9971d2..300a4663e7 100644 --- a/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee @@ -15,7 +15,7 @@ describe "AuthenticationController", -> tk.freeze(Date.now()) @AuthenticationController = SandboxedModule.require modulePath, requires: "./AuthenticationManager": @AuthenticationManager = {} - "../User/UserUpdater" : @UserUpdater = {} + "../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()} "metrics-sharelatex": @Metrics = { inc: sinon.stub() } "../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() } "../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()} diff --git a/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee new file mode 100644 index 0000000000..941e26df1e --- /dev/null +++ b/services/web/test/unit/coffee/Notifications/NotificationsBuilderTests.coffee @@ -0,0 +1,40 @@ +SandboxedModule = require('sandboxed-module') +assert = require('chai').assert +require('chai').should() +sinon = require('sinon') +modulePath = require('path').join __dirname, '../../../../app/js/Features/Notifications/NotificationsBuilder.js' + +describe 'NotificationsBuilder', -> + user_id = "123nd3ijdks" + + beforeEach -> + @handler = + createNotification: sinon.stub().callsArgWith(6) + + @settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } } + @body = {id: 1, name: 'stanford', enrolment_ad_html: 'v1 ad content'} + response = {statusCode: 200} + @request = sinon.stub().returns(@stubResponse).callsArgWith(1, null, response, @body) + @controller = SandboxedModule.require modulePath, requires: + "./NotificationsHandler":@handler + "settings-sharelatex":@settings + 'request': @request + "logger-sharelatex": + log:-> + err:-> + + it 'should call v1 and create affiliation notifications', (done)-> + ip = '192.168.0.1' + @controller.ipMatcherAffiliation(user_id, ip).create (callback)=> + @request.calledOnce.should.equal true + expectedOpts = + university_id: @body.id + university_name: @body.name + content: @body.enrolment_ad_html + @handler.createNotification.calledWith( + user_id, + "ip-matched-affiliation-#{ip}", + "notification_ip_matched_affiliation", + expectedOpts + ).should.equal true + done() diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index b909376cca..f7edc94ad1 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -69,6 +69,10 @@ describe "ProjectController", -> @CollaboratorsHandler = userIsTokenMember: sinon.stub().callsArgWith(2, null, false) @ProjectEntityHandler = {} + @NotificationBuilder = + ipMatcherAffiliation: sinon.stub().returns({create: sinon.stub()}) + @UserGetter = + getUser: sinon.stub().callsArgWith 2, null, {lastLoginIp: '192.170.18.2'} @Modules = hooks: fire: sinon.stub() @@ -105,11 +109,16 @@ describe "ProjectController", -> "./ProjectEntityHandler": @ProjectEntityHandler "../Errors/Errors": Errors "../../infrastructure/Features": @Features + "../Notifications/NotificationsBuilder":@NotificationBuilder + "../User/UserGetter": @UserGetter @projectName = "£12321jkj9ujkljds" @req = params: Project_id: @project_id + headers: {} + connection: + remoteAddress: "192.170.18.1" session: user: @user body: @@ -301,6 +310,13 @@ describe "ProjectController", -> done() @ProjectController.projectListPage @req, @res + it "should create trigger ip matcher notifications", (done)-> + @settings.overleaf = true + @res.render = (pageName, opts)=> + @NotificationBuilder.ipMatcherAffiliation.called.should.equal true + done() + @ProjectController.projectListPage @req, @res + it "should send the projects", (done)-> @res.render = (pageName, opts)=> opts.projects.length.should.equal (@projects.length + @collabertions.length + @readOnly.length + @tokenReadAndWrite.length + @tokenReadOnly.length)