diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index a2444d34d7..b748085daf 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -9,6 +9,7 @@ DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') EditorRealTimeController = require("./EditorRealTimeController") async = require('async') LockManager = require("../../infrastructure/LockManager") +PublicAccessLevels = require("../Authorization/PublicAccessLevels") _ = require('underscore') module.exports = EditorController = @@ -200,8 +201,22 @@ module.exports = EditorController = setPublicAccessLevel : (project_id, newAccessLevel, callback = (err) ->) -> ProjectDetailsHandler.setPublicAccessLevel project_id, newAccessLevel, (err) -> return callback(err) if err? - EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel - callback() + EditorRealTimeController.emitToRoom( + project_id, + 'project:publicAccessLevel:changed', + {newAccessLevel} + ) + if newAccessLevel == PublicAccessLevels.TOKEN_BASED + ProjectDetailsHandler.ensureTokensArePresent project_id, (err, tokens) -> + return callback(err) if err? + EditorRealTimeController.emitToRoom( + project_id, + 'project:tokens:changed', + {tokens} + ) + callback() + else + callback() setRootDoc: (project_id, newRootDocID, callback = (err) ->) -> ProjectEntityHandler.setRootDoc project_id, newRootDocID, (err) -> diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index 70f26157df..eb4e92d669 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -6,6 +6,7 @@ tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender' _ = require("underscore") PublicAccessLevels = require("../Authorization/PublicAccessLevels") Errors = require("../Errors/Errors") +ProjectTokenGenerator = require('./ProjectTokenGenerator') module.exports = ProjectDetailsHandler = getDetails: (project_id, callback)-> @@ -65,6 +66,25 @@ module.exports = ProjectDetailsHandler = setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> logger.log project_id: project_id, level: newAccessLevel, "set public access level" # TODO: remove the read-only and read-and-write items from here - if project_id? && newAccessLevel? and _.include [PublicAccessLevels.READ_ONLY, PublicAccessLevels.READ_AND_WRITE, PublicAccessLevels.PRIVATE, PublicAccessLevels.TOKEN_BASED], newAccessLevel + if project_id? && newAccessLevel? and _.include [ + PublicAccessLevels.READ_ONLY, + PublicAccessLevels.READ_AND_WRITE, + PublicAccessLevels.PRIVATE, + PublicAccessLevels.TOKEN_BASED + ], newAccessLevel Project.update {_id:project_id},{publicAccesLevel:newAccessLevel}, (err)-> callback() + + ensureTokensArePresent: (project_id, callback=(err, tokens)->) -> + ProjectGetter.getProject project_id, {tokens: 1}, (err, project) -> + return callback(err) if err? + if project.tokens? and project.tokens.readOnly? and project.tokens.readAndWrite? + return callback(null, project.tokens) + else + tokens = + readOnly: ProjectTokenGenerator.readOnlyToken() + readAndWrite: ProjectTokenGenerator.readAndWriteToken() + Project.update {_id: project_id}, {$set: {tokens: tokens}}, (err) -> + return callback(err) if err? + callback(null, tokens) + diff --git a/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee b/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee new file mode 100644 index 0000000000..7457929d32 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectTokenGenerator.coffee @@ -0,0 +1,16 @@ +module.exports = ProjectTokenGenerator = + + + readOnlyToken: () -> + length = 12 + tokenAlpha = 'bcdfghjkmnpqrstvwxyz' + result = '' + for _n in [1..length] + i = Math.floor(Math.floor(Math.random() * tokenAlpha.length)) + result += tokenAlpha[i] + return result + + readAndWriteToken: () -> + numerics = Math.random().toString().slice(2, 12) + token = ProjectTokenGenerator.readOnlyToken() + return "#{numerics}#{token}" diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 46a6711581..45e4bcf505 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -6,14 +6,7 @@ logger = require('logger-sharelatex') sanitize = require('sanitizer') concreteObjectId = require('mongoose').Types.ObjectId Errors = require "../Features/Errors/Errors" - -generateToken = (length) -> - tokenAlpha = 'bcdfghjkmnpqrstvwxyz' - result = '' - for _n in [1..length] - i = Math.floor(Math.floor(Math.random() * tokenAlpha.length)) - result += tokenAlpha[i] - return result +ProjectTokenGenerator = require '../Features/Project/ProjectTokenGenerator' Schema = mongoose.Schema ObjectId = Schema.ObjectId @@ -43,13 +36,11 @@ ProjectSchema = new Schema tokens : readOnly : { type: String, - default: generateToken(14), - index: {unique: true} + default: ProjectTokenGenerator.readOnlyToken() } readAndWrite : { type: String, - default: Math.random().toString().slice(2, 10) + generateToken(12) - index: {unique: true} + default: ProjectTokenGenerator.readAndWriteToken() } tokenAccessReadOnly_refs : [ type:ObjectId, ref:'User' ] tokenAccessReadAndWrite_refs : [ type:ObjectId, ref:'User' ] diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 1ac3bf8709..d33339f359 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -12,6 +12,16 @@ define [ scope: $scope ) + ide.socket.on 'project:tokens:changed', (data) => + if data.tokens? + ide.$scope.project.tokens = data.tokens + $scope.$digest() + + ide.socket.on 'project:publicAccessLevel:changed', (data) => + if data.newAccessLevel? + ide.$scope.project.publicAccesLevel = data.newAccessLevel + $scope.$digest() + ide.socket.on 'project:membership:changed', (data) => if data.members projectMembers.getMembers() diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 39f6438cef..48a16c6bdf 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http) -> + App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide) -> $scope.inputs = { privileges: "readAndWrite" contacts: [] @@ -200,10 +200,16 @@ define [ } $scope.getReadAndWriteTokenLink = () -> - location.origin + "/" + $scope.project.tokens.readAndWrite + if $scope?.project?.tokens?.readAndWrite? + location.origin + "/" + $scope.project.tokens.readAndWrite + else + '' $scope.getReadOnlyTokenLink = () -> - location.origin + "/read/" + $scope.project.tokens.readOnly + if $scope?.project?.tokens?.readOnly? + location.origin + "/read/" + $scope.project.tokens.readOnly + else + '' $scope.done = () -> $modalInstance.close() diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee index 0b7bfb1d2b..1c69b375b8 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee @@ -564,20 +564,99 @@ describe "EditorController", -> describe "setPublicAccessLevel", -> - beforeEach -> - @newAccessLevel = "public" - @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub().callsArgWith(2, null) - @EditorRealTimeController.emitToRoom = sinon.stub() + describe 'when setting to private', -> + beforeEach -> + @newAccessLevel = 'private' + @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub().callsArgWith(2, null) + @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub() + .callsArgWith(1, null, @tokens) + @EditorRealTimeController.emitToRoom = sinon.stub() - it "should call the EditorController", (done)-> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @ProjectDetailsHandler.setPublicAccessLevel.calledWith(@project_id, @newAccessLevel).should.equal true - done() + it 'should set the access level', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @ProjectDetailsHandler.setPublicAccessLevel + .calledWith(@project_id, @newAccessLevel).should.equal true + done() - it "should emit the update to the room", (done)-> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'publicAccessLevelUpdated', @newAccessLevel).should.equal true - done() + it 'should broadcast the access level change', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:publicAccessLevel:changed').should.equal true + done() + + it 'should not ensure tokens are present for project', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @ProjectDetailsHandler.ensureTokensArePresent + .calledWith(@project_id).should.equal false + done() + + it 'should not broadcast a token change', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) + .should.equal false + done() + + it 'should not produce an error', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, (err) => + expect(err).to.not.exist + done() + + describe 'when setting to tokenBased', -> + beforeEach -> + @newAccessLevel = 'tokenBased' + @tokens = {readOnly: 'aaa', readAndWrite: '42bbb'} + @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub() + .callsArgWith(2, null) + @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub() + .callsArgWith(1, null, @tokens) + @EditorRealTimeController.emitToRoom = sinon.stub() + + it 'should set the access level', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @ProjectDetailsHandler.setPublicAccessLevel + .calledWith(@project_id, @newAccessLevel).should.equal true + done() + + it 'should broadcast the access level change', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:publicAccessLevel:changed') + .should.equal true + done() + + it 'should ensure tokens are present for project', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @ProjectDetailsHandler.ensureTokensArePresent + .calledWith(@project_id).should.equal true + done() + + it 'should broadcast the token change too', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) + .should.equal true + done() + + it 'should not produce an error', (done) -> + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, (err) => + expect(err).to.not.exist + done() + + # beforeEach -> + # @newAccessLevel = "public" + # @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub().callsArgWith(2, null) + # @EditorRealTimeController.emitToRoom = sinon.stub() + + # it "should call the EditorController", (done)-> + # @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + # @ProjectDetailsHandler.setPublicAccessLevel.calledWith(@project_id, @newAccessLevel).should.equal true + # done() + + # it "should emit the update to the room", (done)-> + # @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + # @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'publicAccessLevelUpdated', @newAccessLevel).should.equal true + # done() describe "setRootDoc", -> @@ -594,4 +673,4 @@ describe "EditorController", -> it "should emit the update to the room", (done)-> @EditorController.setRootDoc @project_id, @newRootDocID, => @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'rootDocUpdated', @newRootDocID).should.equal true - done() \ No newline at end of file + done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee index c4c3b3ef07..59487e3845 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee @@ -38,6 +38,7 @@ describe 'ProjectDetailsHandler', -> 'logger-sharelatex': log:-> err:-> + './ProjectTokenGenerator': @ProjectTokenGenerator = {} describe "getDetails", -> @@ -149,3 +150,76 @@ describe 'ProjectDetailsHandler', -> @ProjectModel.update.calledWith({_id: @project_id}, {publicAccesLevel: @accessLevel}).should.equal true done() + describe "ensureTokensArePresent", -> + beforeEach -> + + describe 'when the project has tokens', -> + beforeEach -> + @project = + _id: @project_id + tokens: + readOnly: 'aaa' + readAndWrite: '42bbb' + @ProjectGetter.getProject = sinon.stub() + .callsArgWith(2, null, @project) + @ProjectModel.update = sinon.stub() + + it 'should get the project', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(@ProjectGetter.getProject.callCount).to.equal 1 + expect(@ProjectGetter.getProject.calledWith(@project_id, {tokens: 1})) + .to.equal true + done() + + it 'should not update the project with new tokens', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(@ProjectModel.update.callCount).to.equal 0 + done() + + it 'should produce the tokens without error', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(err).to.not.exist + expect(tokens).to.deep.equal @project.tokens + done() + + describe 'when tokens are missing', -> + beforeEach -> + @project = + _id: @project_id + @ProjectGetter.getProject = sinon.stub() + .callsArgWith(2, null, @project) + @readOnlyToken = 'abc' + @readAndWriteToken = '42def' + @ProjectTokenGenerator.readOnlyToken = sinon.stub().returns(@readOnlyToken) + @ProjectTokenGenerator.readAndWriteToken = sinon.stub().returns(@readAndWriteToken) + @ProjectModel.update = sinon.stub() + .callsArgWith(2, null) + + it 'should get the project', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(@ProjectGetter.getProject.callCount).to.equal 1 + expect(@ProjectGetter.getProject.calledWith(@project_id, {tokens: 1})) + .to.equal true + done() + + it 'should update the project with new tokens', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(@ProjectTokenGenerator.readOnlyToken.callCount) + .to.equal 1 + expect(@ProjectTokenGenerator.readAndWriteToken.callCount) + .to.equal 1 + expect(@ProjectModel.update.callCount).to.equal 1 + expect(@ProjectModel.update.calledWith( + {_id: @project_id}, + {$set: {tokens: {readOnly: @readOnlyToken, readAndWrite: @readAndWriteToken}}} + )).to.equal true + done() + + it 'should produce the tokens without error', (done) -> + @handler.ensureTokensArePresent @project_id, (err, tokens) => + expect(err).to.not.exist + expect(tokens).to.deep.equal { + readOnly: @readOnlyToken, + readAndWrite: @readAndWriteToken + } + done()