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/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index f33f3e523e..94aeaa2a90 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -31,56 +31,56 @@ UnsupportedFileTypeError = (message) -> error.name = "UnsupportedFileTypeError" error.__proto__ = UnsupportedFileTypeError.prototype return error -UnsupportedFileTypeError.prototype.__proto___ = Error.prototype +UnsupportedFileTypeError.prototype.__proto__ = Error.prototype UnsupportedBrandError = (message) -> error = new Error(message) error.name = "UnsupportedBrandError" error.__proto__ = UnsupportedBrandError.prototype return error -UnsupportedBrandError.prototype.__proto___ = Error.prototype +UnsupportedBrandError.prototype.__proto__ = Error.prototype UnsupportedExportRecordsError = (message) -> error = new Error(message) error.name = "UnsupportedExportRecordsError" error.__proto__ = UnsupportedExportRecordsError.prototype return error -UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype +UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype V1HistoryNotSyncedError = (message) -> error = new Error(message) error.name = "V1HistoryNotSyncedError" error.__proto__ = V1HistoryNotSyncedError.prototype return error -V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype +V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype ProjectHistoryDisabledError = (message) -> error = new Error(message) error.name = "ProjectHistoryDisabledError" error.__proto__ = ProjectHistoryDisabledError.prototype return error -ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype +ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype V1ConnectionError = (message) -> error = new Error(message) error.name = "V1ConnectionError" error.__proto__ = V1ConnectionError.prototype return error -V1ConnectionError.prototype.__proto___ = Error.prototype +V1ConnectionError.prototype.__proto__ = Error.prototype UnconfirmedEmailError = (message) -> error = new Error(message) error.name = "UnconfirmedEmailError" error.__proto__ = UnconfirmedEmailError.prototype return error -UnconfirmedEmailError.prototype.__proto___ = Error.prototype +UnconfirmedEmailError.prototype.__proto__ = Error.prototype EmailExistsError = (message) -> error = new Error(message) error.name = "EmailExistsError" error.__proto__ = EmailExistsError.prototype return error -EmailExistsError.prototype.__proto___ = Error.prototype +EmailExistsError.prototype.__proto__ = Error.prototype module.exports = Errors = NotFoundError: NotFoundError 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/infrastructure/ProxyManager.coffee b/services/web/app/coffee/infrastructure/ProxyManager.coffee index 4d64b25bae..3e7a037054 100644 --- a/services/web/app/coffee/infrastructure/ProxyManager.coffee +++ b/services/web/app/coffee/infrastructure/ProxyManager.coffee @@ -7,14 +7,20 @@ module.exports = ProxyManager = apply: (publicApiRouter) -> for proxyUrl, target of settings.proxyUrls do (target) -> - publicApiRouter.get proxyUrl, ProxyManager.createProxy(target) + method = target.options?.method || 'get' + publicApiRouter[method] proxyUrl, ProxyManager.createProxy(target) createProxy: (target) -> (req, res, next) -> targetUrl = makeTargetUrl(target, req) logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url" - upstream = request(targetUrl) + options = + url: targetUrl + options.headers = { Cookie: req.headers.cookie } if req.headers?.cookie + Object.assign(options, target.options) if target?.options? + options.form = req.body if options.method in ['post', 'put'] + upstream = request(options) upstream.on "error", (error) -> logger.error err: error, "error in ProxyManager" 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/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index e3ab0b29be..015a4e65ee 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -516,6 +516,7 @@ define [ scope.$on '$destroy', () -> if scope.sharejsDoc? + scope.$broadcast('changeEditor') tearDownSpellCheck() tearDownCursorPosition() detachFromAce(scope.sharejsDoc) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index bb957f8cf6..764fcb1c98 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -3,12 +3,12 @@ define [], () -> constructor: (@$scope, @adapter, @localStorage) -> @$scope.$on 'editorInit', @jumpToPositionInNewDoc - @$scope.$on 'beforeChangeDocument', () => - @storeCursorPosition() - @storeFirstVisibleLine() + @$scope.$on 'beforeChangeDocument', @storePositionAndLine @$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc + @$scope.$on 'changeEditor', @storePositionAndLine + @$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) => if line? setTimeout () => @@ -24,6 +24,10 @@ define [], () -> @$scope.$on "#{@$scope.name}:clearSelection", (e) => @adapter.clearSelection() + storePositionAndLine: () => + @storeCursorPosition() + @storeFirstVisibleLine() + jumpToPositionInNewDoc: () => @doc_id = @$scope.sharejsDoc?.doc_id setTimeout () => diff --git a/services/web/public/stylesheets/app/cms-page.less b/services/web/public/stylesheets/app/cms-page.less index f6cdf734d4..e6a3778cda 100644 --- a/services/web/public/stylesheets/app/cms-page.less +++ b/services/web/public/stylesheets/app/cms-page.less @@ -4,6 +4,10 @@ including About and Blog */ .cms-page { + img { + height: auto; + max-width: 100%; + } .btn-description { margin-right: @margin-sm; } diff --git a/services/web/public/stylesheets/app/editor/publish-modal.less b/services/web/public/stylesheets/app/editor/publish-modal.less index 3f049fc75b..dbf49a945d 100644 --- a/services/web/public/stylesheets/app/editor/publish-modal.less +++ b/services/web/public/stylesheets/app/editor/publish-modal.less @@ -1,4 +1,32 @@ .modal-body-publish { + .form-control-box { + margin-bottom: 1.5ex; + margin-left: 1.0em; + label { + display: inline-block; + width: 10em; + vertical-align: baseline; + } + .form-control { + display: inline-block; + width: 60%; + } + input[type="checkbox"] { + margin-right: 0.5em; + } + textarea { + vertical-align: baseline; + } + select { + padding-top: 1ex; + padding-bottom: 1ex; + padding-left: 1em; + padding-right: 1em; + } + option { + margin-left: -4px; + } + } #search-input-container { overflow: hidden; margin: 5px 0 10px; diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less index ae6fcdf230..5e987f9d58 100644 --- a/services/web/public/stylesheets/app/portals.less +++ b/services/web/public/stylesheets/app/portals.less @@ -100,52 +100,10 @@ } // End Print - /* - Begin Tabs - */ .nav-tabs { - // Overrides for nav.less background-color: @ol-blue-gray-0; - border: 0!important; - margin-bottom: @margin-md; - margin-top: -@line-height-computed; //- adjusted for portal-name - padding: @padding-lg 0 @padding-md; - text-align: center; - - a { - color: @link-color; - &:hover { - background-color: transparent!important; - border: 0!important; - color: @link-hover-color!important; - } - } - - li { - display: inline-block; - float: none; - a { - border: 0; - } - } - - li.active > a { - background-color: transparent!important; - border: 0; - border-bottom: 1px solid @accent-color-secondary!important; - color: @accent-color-secondary; - &:hover { - color: @accent-color-secondary!important; - } - } } - .tab-content:extend(.container) { - background-color: transparent!important; - border: none!important; - } - // End Tabs - @media (max-width: @screen-size-sm-max) { .content-pull { padding: 0; diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index 29ccf505e6..fabdaa232f 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -15,6 +15,7 @@ } img { + height: auto; max-width: 100%; } diff --git a/services/web/public/stylesheets/components/tabs.less b/services/web/public/stylesheets/components/tabs.less new file mode 100644 index 0000000000..3fa4fb106c --- /dev/null +++ b/services/web/public/stylesheets/components/tabs.less @@ -0,0 +1,43 @@ +.ol-tabs { + // Overrides for nav.less + .nav-tabs { + border: 0!important; + margin-bottom: @margin-md; + margin-top: -@line-height-computed; //- adjusted for portal-name + padding: @padding-lg 0 @padding-md; + text-align: center; + } + + a { + color: @link-color; + &:hover { + background-color: transparent!important; + border: 0!important; + color: @link-hover-color!important; + } + } + + li { + display: inline-block; + float: none; + a { + border: 0; + } + } + + li.active > a { + background-color: transparent!important; + border: 0!important; + border-bottom: 1px solid @accent-color-secondary!important; + color: @accent-color-secondary!important; + &:hover { + color: @accent-color-secondary!important; + } + } + .tab-content:extend(.container) { + background-color: transparent!important; + border: none!important; + } +} + + \ No newline at end of file diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index be8a97ea98..88c6953509 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -10,6 +10,7 @@ @import "components/icons.less"; @import "components/navs-ol.less"; @import "components/pagination.less"; +@import "components/tabs.less"; // Pages @import "app/about.less"; diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index b8740f709c..691a214c8b 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -80,6 +80,9 @@ class User update["features.#{key}"] = value UserModel.update { _id: @id }, update, callback + setOverleafId: (overleaf_id, callback = (error) ->) -> + UserModel.update { _id: @id }, { 'overleaf.id': overleaf_id }, callback + logout: (callback = (error) ->) -> @getCsrfToken (error) => return callback(error) if error? 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) diff --git a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee index 3f1c1e9f78..9a1d9ea17c 100644 --- a/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee +++ b/services/web/test/unit/coffee/infrastructure/ProxyManagerTests.coffee @@ -35,11 +35,26 @@ describe "ProxyManager", -> assertCalledWith(@router.get, '/foo/bar') assertCalledWith(@router.get, '/foo/:id') + it 'applies methods other than get', -> + @router = + post: sinon.stub() + put: sinon.stub() + @settings.proxyUrls = + '/foo/bar': {options: {method: 'post'}} + '/foo/:id': {options: {method: 'put'}} + @proxyManager.apply(@router) + sinon.assert.calledOnce(@router.post) + sinon.assert.calledOnce(@router.put) + assertCalledWith(@router.post, '/foo/bar') + assertCalledWith(@router.put, '/foo/:id') + describe 'createProxy', -> beforeEach -> @req.url = @proxyPath @req.route.path = @proxyPath @req.query = {} + @req.params = {} + @req.headers = {} @settings.proxyUrls = {} afterEach -> @@ -57,7 +72,7 @@ describe "ProxyManager", -> targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash' @settings.proxyUrls[@proxyPath] = targetUrl @proxyManager.createProxy(targetUrl)(@req) - assertCalledWith(@request, targetUrl) + assertCalledWith(@request, {url: targetUrl}) it 'overwrite query', -> targetUrl = 'foo.bar/baz?query' @@ -65,19 +80,19 @@ describe "ProxyManager", -> @settings.proxyUrls[@proxyPath] = targetUrl @proxyManager.createProxy(targetUrl)(@req) newTargetUrl = 'foo.bar/baz?requestQuery=important' - assertCalledWith(@request, newTargetUrl) + assertCalledWith(@request, {url: newTargetUrl}) it 'handles target objects', -> target = { baseUrl: 'api.v1', path: '/pa/th'} @settings.proxyUrls[@proxyPath] = target @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'api.v1/pa/th') + assertCalledWith(@request, {url: 'api.v1/pa/th'}) it 'handles missing baseUrl', -> target = { path: '/pa/th'} @settings.proxyUrls[@proxyPath] = target @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'undefined/pa/th') + assertCalledWith(@request, {url: 'undefined/pa/th'}) it 'handles dynamic path', -> target = baseUrl: 'api.v1', path: (params) -> "/resource/#{params.id}" @@ -86,4 +101,49 @@ describe "ProxyManager", -> @req.route.path = '/res/:id' @req.params = id: 123 @proxyManager.createProxy(target)(@req, @res, @next) - assertCalledWith(@request, 'api.v1/resource/123') + assertCalledWith(@request, {url: 'api.v1/resource/123'}) + + it 'set arbitrary options on request', -> + target = baseUrl: 'api.v1', path: '/foo', options: foo: 'bar' + @req.url = '/foo' + @req.route.path = '/foo' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + foo: 'bar' + url: 'api.v1/foo' + ) + + it 'passes cookies', -> + target = baseUrl: 'api.v1', path: '/foo' + @req.url = '/foo' + @req.route.path = '/foo' + @req.headers = cookie: 'cookie' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + headers: Cookie: 'cookie' + url: 'api.v1/foo' + ) + + it 'passes body for post', -> + target = baseUrl: 'api.v1', path: '/foo', options: method: 'post' + @req.url = '/foo' + @req.route.path = '/foo' + @req.body = foo: 'bar' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + form: foo: 'bar' + method: 'post' + url: 'api.v1/foo' + ) + + it 'passes body for put', -> + target = baseUrl: 'api.v1', path: '/foo', options: method: 'put' + @req.url = '/foo' + @req.route.path = '/foo' + @req.body = foo: 'bar' + @proxyManager.createProxy(target)(@req, @res, @next) + assertCalledWith(@request, + form: foo: 'bar' + method: 'put' + url: 'api.v1/foo' + ) \ No newline at end of file