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

Add account-reconciliation script

GitOrigin-RevId: e20a8b9531126d91baaa9da5c77ab6e971b43e67
This commit is contained in:
Simon Detheridge 2019-05-16 11:11:42 +01:00 committed by sharelatex
parent 6a45be9ccd
commit c07c44f366
9 changed files with 118 additions and 37 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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