mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #1644 from overleaf/spd-reconcile-accounts
Add account-reconciliation script GitOrigin-RevId: e20a8b9531126d91baaa9da5c77ab6e971b43e67
This commit is contained in:
parent
6a45be9ccd
commit
c07c44f366
9 changed files with 118 additions and 37 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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", ->
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue