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

Merge pull request #1644 from overleaf/spd-reconcile-accounts

Add account-reconciliation script

GitOrigin-RevId: e20a8b9531126d91baaa9da5c77ab6e971b43e67
Simon Detheridge 7 лет назад
Родитель
Сommit
c07c44f366

+ 64 - 0
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -150,6 +150,70 @@ templates.testEmail = CTAEmailTemplate({
 	ctaURL: () -> settings.siteUrl
 })
 
+templates.projectsTransferredFromSharelatex = CTAEmailTemplate({
+	subject: () -> "ShareLaTeX projects transferred to your Overleaf account"
+	title: () -> "ShareLaTeX projects transferred to your Overleaf account"
+	message: (opts) -> """
+We are writing with important information about your Overleaf and ShareLaTeX accounts.
+
+As part of our ongoing work to [integrate Overleaf and ShareLaTeX](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf),
+we found a ShareLaTeX account with the email address #{opts.to} that matches your Overleaf account.
+
+We have now transferred the projects from this ShareLaTeX account into your Overleaf account, so you may notice some new
+projects on your Overleaf projects page.
+
+When you next log in, you may be prompted to reconfirm your email address in order to regain access to your account.
+If you have any questions, please contact our support team by reply.
+"""
+	ctaText: () -> "Log in to #{ settings.appName }"
+	ctaURL: () -> settings.siteUrl + "login"
+})
+
+templates.emailAddressPoachedEmail = CTAEmailTemplate({
+	subject: () -> "One of your email addresses has been moved to another #{ settings.appName } account"
+	title: () -> "One of your email addresses has been moved to another #{ settings.appName } account"
+	message: (opts) ->
+		message = """
+We are writing with important information about your Overleaf account.
+
+You added the email address #{opts.poached} to your #{opts.to} Overleaf account as a secondary (or affiliation)
+email address, but we have had to remove it.
+
+This is because your #{opts.poached} email address was also in use as the primary email address for an older Overleaf
+account from before our [integration with ShareLaTeX to create Overleaf v2](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf).
+
+### What do I need to do?
+
+You now have two Overleaf accounts, one under #{opts.poached} and one under #{opts.to}.
+
+You may wish to log in to Overleaf as #{opts.poached} to check whether you have projects there that you would like to
+keep. If you are not sure of the password, you can send yourself a password reset email to #{opts.poached}, via
+https://www.overleaf.com/user/password/reset
+
+Once you have downloaded your projects, you may wish to delete your
+#{opts.poached} Overleaf account, which you can do from your account settings. You will then be able to add
+#{opts.poached} as a secondary email address on your #{opts.to} account again.
+
+
+"""
+		if opts.proFeatures
+			message += """
+Because your #{opts.poached} email address was an institutional affiliation through which you had Pro features. Your Pro
+features have been transferred to your #{opts.poached} account. If you would like to transfer them back to your
+#{opts.to} account, you will need to delete the #{opts.poached} account and re-add it as a secondary email address,
+as described above.
+
+
+"""
+
+		message += """
+If you have any questions, you can contact our support team by reply.
+"""
+		return message
+	ctaText: () -> "Log in to #{ settings.appName }"
+	ctaURL: () -> settings.siteUrl + "login"
+})
+
 module.exports =
 	templates: templates
 	CTAEmailTemplate: CTAEmailTemplate

+ 4 - 4
services/web/app/coffee/Features/Project/ProjectDeleter.coffee

@@ -28,9 +28,9 @@ module.exports = ProjectDeleter =
 		logger.log {user_id}, "deleting users projects"
 		ProjectDeleter._deleteUsersProjectWithMethod user_id, ProjectDeleter.deleteProject, callback
 
-	softDeleteUsersProjects: (user_id, callback)->
+	softDeleteUsersProjectsForMigration: (user_id, callback)->
 		logger.log {user_id}, "soft-deleting users projects"
-		ProjectDeleter._deleteUsersProjectWithMethod user_id, ProjectDeleter.softDeleteProject, callback
+		ProjectDeleter._deleteUsersProjectWithMethod user_id, ProjectDeleter.softDeleteProjectForMigration, callback
 
 	_deleteUsersProjectWithMethod: (user_id, deleteMethod, callback) ->
 		Project.find {owner_ref: user_id}, (error, projects) ->
@@ -44,7 +44,7 @@ module.exports = ProjectDeleter =
 					CollaboratorsHandler.removeUserFromAllProjets user_id, callback
 			)
 
-	softDeleteProject: (project_id, callback) ->
+	softDeleteProjectForMigration: (project_id, callback) ->
 		logger.log project_id: project_id, "soft-deleting project"
 		async.waterfall [
 			(cb) ->
@@ -52,7 +52,7 @@ module.exports = ProjectDeleter =
 			(project, cb) ->
 				return callback(new Errors.NotFoundError("project not found")) unless project?
 				project.deletedAt = new Date()
-				db.deletedProjects.insert project, (err) -> cb(err)
+				db.projectsDeletedByMigration.insert project, (err) -> cb(err)
 			(cb) ->
 				ProjectDeleter.deleteProject project_id, cb
 		], callback

+ 6 - 3
services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee

@@ -47,8 +47,11 @@ module.exports = ProjectDetailsHandler =
 				logger.err err:err, "something went wrong setting project description"
 			callback(err)
 
-	transferOwnership: (project_id, user_id, callback)->
-		ProjectGetter.getProject project_id, {owner_ref: true, name: true}, (err, project)->
+	transferOwnership: (project_id, user_id, suffix = "", callback)->
+		if typeof suffix is 'function'
+			callback = suffix
+			suffix = ''
+		ProjectGetter.getProject project_id, {owner_ref: true}, (err, project)->
 			return callback(err) if err?
 			return callback(new Errors.NotFoundError("project not found")) unless project?
 			return callback() if project.owner_ref == user_id
@@ -57,7 +60,7 @@ module.exports = ProjectDetailsHandler =
 				return callback(err) if err?
 				return callback(new Errors.NotFoundError("user not found")) unless user?
 
-				ProjectDetailsHandler.generateUniqueName user_id, project.name, (err, name) ->
+				ProjectDetailsHandler.generateUniqueName user_id, project.name + suffix, (err, name) ->
 					return callback(err) if err?
 
 					Project.update {_id: project_id},

+ 3 - 3
services/web/app/coffee/Features/User/UserDeleter.coffee

@@ -12,7 +12,7 @@ Errors = require("../Errors/Errors")
 
 module.exports = UserDeleter =
 
-	softDeleteUser: (user_id, callback = (err)->)->
+	softDeleteUserForMigration: (user_id, callback = (err)->)->
 		if !user_id?
 			logger.err "user_id is null when trying to delete user"
 			return callback(new Error("no user_id"))
@@ -23,10 +23,10 @@ module.exports = UserDeleter =
 				(cb) ->
 					UserDeleter._cleanupUser user, cb
 				(cb) ->
-					ProjectDeleter.softDeleteUsersProjects user._id, cb
+					ProjectDeleter.softDeleteUsersProjectsForMigration user._id, cb
 				(cb) ->
 					user.deletedAt = new Date()
-					db.deletedUsers.insert user, cb
+					db.usersDeletedByMigration.insert user, cb
 				(cb) ->
 					user.remove cb
 			], callback)

+ 5 - 0
services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee

@@ -14,6 +14,11 @@ module.exports = UserEmailsConfirmationHandler =
 		if arguments.length == 3
 			callback = emailTemplate
 			emailTemplate = 'confirmEmail'
+
+		# when force-migrating accounts to v2 from v1, we don't want to send confirmation messages -
+		# setting this env var allows us to turn this behaviour off
+		return callback(null) if process.env['SHARELATEX_NO_CONFIRMATION_MESSAGES']?
+
 		email = EmailHelper.parseEmail(email)
 		return callback(new Error('invalid email')) if !email?
 		data = {user_id, email}

+ 1 - 0
services/web/app/coffee/models/User.coffee

@@ -79,6 +79,7 @@ UserSchema = new Schema
 		refreshToken: { type: String }
 	awareOfV2: { type:Boolean, default: false }
 	thirdPartyIdentifiers: { type: Array, default: [] }
+	migratedAt: { type: Date }
 
 conn = mongoose.createConnection(Settings.mongo.url, {
 	server: {poolSize: Settings.mongo.poolSize || 10},

+ 17 - 15
services/web/test/unit/coffee/Project/ProjectDeleterTests.coffee

@@ -22,6 +22,8 @@ describe 'ProjectDeleter', ->
 			db:
 				deletedProjects:
 					insert: sinon.stub().callsArg(1)
+				projectsDeletedByMigration:
+					insert: sinon.stub().callsArg(1)
 	
 		@Project = 
 			update: sinon.stub().callsArgWith(3)
@@ -105,32 +107,32 @@ describe 'ProjectDeleter', ->
 				@CollaboratorsHandler.removeUserFromAllProjets.calledWith(@user_id).should.equal true
 				done()
 
-	describe "softDeleteUsersProjects", ->
+	describe "softDeleteUsersProjectsForMigrationForMigration", ->
 		beforeEach ->
-			@deleter.softDeleteProject = sinon.stub().callsArg(1)
+			@deleter.softDeleteProjectForMigration = sinon.stub().callsArg(1)
 
 		it "should find all the projects owned by the user_id", (done)->
-			@deleter.softDeleteUsersProjects @user_id, =>
+			@deleter.softDeleteUsersProjectsForMigration @user_id, =>
 				@Project.find.calledWith(owner_ref: @user_id).should.equal true
 				done()
 
 		it "should call deleteProject on the found projects", (done)->
-			@deleter.softDeleteUsersProjects @user_id, =>
-				sinon.assert.calledWith(@deleter.softDeleteProject, @project._id)
+			@deleter.softDeleteUsersProjectsForMigration @user_id, =>
+				sinon.assert.calledWith(@deleter.softDeleteProjectForMigration, @project._id)
 				done()
 
 		it "should call deleteProject once for each project", (done)->
 			@Project.find.callsArgWith(1, null, [
 				{_id: 'potato'}, {_id: 'wombat'}
 			])
-			@deleter.softDeleteUsersProjects @user_id, =>
-				sinon.assert.calledTwice(@deleter.softDeleteProject)
-				sinon.assert.calledWith(@deleter.softDeleteProject, 'wombat')
-				sinon.assert.calledWith(@deleter.softDeleteProject, 'potato')
+			@deleter.softDeleteUsersProjectsForMigration @user_id, =>
+				sinon.assert.calledTwice(@deleter.softDeleteProjectForMigration)
+				sinon.assert.calledWith(@deleter.softDeleteProjectForMigration, 'wombat')
+				sinon.assert.calledWith(@deleter.softDeleteProjectForMigration, 'potato')
 				done()
 
 		it "should remove all the projects the user is a collaborator of", (done)->
-			@deleter.softDeleteUsersProjects @user_id, =>
+			@deleter.softDeleteUsersProjectsForMigration @user_id, =>
 				@CollaboratorsHandler.removeUserFromAllProjets.calledWith(@user_id).should.equal true
 				done()
 
@@ -158,22 +160,22 @@ describe 'ProjectDeleter', ->
 				}).should.equal true
 				done()
 
-	describe "softDeleteProject", ->
+	describe "softDeleteProjectForMigration", ->
 		beforeEach ->
 			@deleter.deleteProject = sinon.stub().callsArg(1)
 
 		it "should set the deletedAt time", (done)->
-			@deleter.softDeleteProject @project_id, =>
+			@deleter.softDeleteProjectForMigration @project_id, =>
 				@project.deletedAt.should.exist
 				done()
 
 		it "should insert the project into the deleted projects collection", (done)->
-			@deleter.softDeleteProject @project_id, =>
-				sinon.assert.calledWith(@mongojs.db.deletedProjects.insert, @project)
+			@deleter.softDeleteProjectForMigration @project_id, =>
+				sinon.assert.calledWith(@mongojs.db.projectsDeletedByMigration.insert, @project)
 				done()
 
 		it "should delete the project", (done)->
-			@deleter.softDeleteProject @project_id, =>
+			@deleter.softDeleteProjectForMigration @project_id, =>
 				sinon.assert.calledWith(@deleter.deleteProject, @project_id)
 				done()
 

+ 4 - 0
services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee

@@ -117,6 +117,10 @@ describe 'ProjectDetailsHandler', ->
 				sinon.assert.calledWith(@handler.generateUniqueName, '123', @project.name)
 				done()
 
+		it "should append the supplied suffix to the project name, if passed", (done) ->
+			@handler.transferOwnership 'abc', '123', ' wombat', () =>
+				sinon.assert.calledWith(@handler.generateUniqueName, '123', "#{@project.name} wombat")
+				done()
 
 	describe "getProjectDescription", ->
 

+ 14 - 12
services/web/test/unit/coffee/User/UserDeleterTests.coffee

@@ -20,7 +20,7 @@ describe "UserDeleter", ->
 
 		@ProjectDeleter =
 			deleteUsersProjects: sinon.stub().callsArgWith(1)
-			softDeleteUsersProjects: sinon.stub().callsArgWith(1)
+			softDeleteUsersProjectsForMigration: sinon.stub().callsArgWith(1)
 
 		@SubscriptionHandler = 
 			cancelSubscription: sinon.stub().callsArgWith(1)
@@ -37,6 +37,8 @@ describe "UserDeleter", ->
 			db:
 				deletedUsers:
 					insert: sinon.stub().callsArg(1)
+				usersDeletedByMigration:
+					insert: sinon.stub().callsArg(1)
 
 		@UserDeleter = SandboxedModule.require modulePath, requires:
 			"../../models/User": User: @User
@@ -50,46 +52,46 @@ describe "UserDeleter", ->
 			"../../infrastructure/mongojs": @mongojs
 			"logger-sharelatex": @logger = { log: sinon.stub() }
 
-	describe "softDeleteUser", ->
+	describe "softDeleteUserForMigration", ->
 
 		it "should delete the user in mongo", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@User.findById.calledWith(@user._id).should.equal true
 				@user.remove.called.should.equal true
 				done()
 
 		it "should add the user to the deletedUsers collection", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
-				sinon.assert.calledWith(@mongojs.db.deletedUsers.insert, @user)
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
+				sinon.assert.calledWith(@mongojs.db.usersDeletedByMigration.insert, @user)
 				done()
 
 		it "should set the deletedAt field on the user", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@user.deletedAt.should.exist
 				done()
 
 		it "should unsubscribe the user from the news letter", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@NewsletterManager.unsubscribe.calledWith(@user).should.equal true
 				done()
 
 		it "should unsubscribe the user", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@SubscriptionHandler.cancelSubscription.calledWith(@user).should.equal true
 				done()
 
 		it "should delete user affiliations", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@deleteAffiliations.calledWith(@user._id).should.equal true
 				done()
 
 		it "should soft-delete all the projects of a user", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
-				@ProjectDeleter.softDeleteUsersProjects.calledWith(@user._id).should.equal true
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
+				@ProjectDeleter.softDeleteUsersProjectsForMigration.calledWith(@user._id).should.equal true
 				done()
 
 		it "should remove user memberships", (done)->
-			@UserDeleter.softDeleteUser @user._id, (err)=>
+			@UserDeleter.softDeleteUserForMigration @user._id, (err)=>
 				@UserMembershipsHandler.removeUserFromAllEntities.calledWith(@user._id).should.equal true
 				done()