Add script that migrates users with email-formatted usernames

This commit is contained in:
Scott Erickson 2016-08-11 23:08:09 -07:00
parent 76e461398e
commit a5f18f88be
5 changed files with 293 additions and 4 deletions

View file

@ -0,0 +1,167 @@
# Usage:
# > coffee -c scripts/node/fixEmailFormattedUsernames.coffee; node scripts/node/fixEmailFormattedUsernames.js run
require('coffee-script');
require('coffee-script/register');
_ = require 'lodash'
sendwithus = require '../../server/sendwithus'
log = require 'winston'
str = require 'underscore.string'
co = require 'co'
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i
changedUsernameTemplate = _.template("
<p>
Hi, CodeCombat user!
</p>
<p>
Just letting you know we've made a change to your account settings which may change how you log in. Here are your old settings:
</p>
<ul>
<li>Old username: <%= oldUsername %></li>
<li>Old email: <%= oldEmail %></li>
</ul>
</p>
Your old username was an email address, but to reduce confusion, we now make sure email addresses can't be used as usernames.
<b><%= specialMessage %></b>
Here are your new settings:
</p>
<ul>
<li>New username: <%= newUsername %></li>
<li>New email: <%= newEmail %></li>
</ul>
<p>
Please <a href='https://codecombat.com/account/settings'>visit the site</a> if you would like to update your settings.
And let us know if you have any questions!
</p>
")
exports.run = ->
co ->
mongoose = require 'mongoose'
User = require '../../server/models/User'
users = yield User.find({nameLower: {$regex: filter}}).select({name:1, email:1, anonymous:1, slug:1})
console.log 'found', users.length, 'users'
for user in users
oldUsername = user.get('name')
oldEmail = user.get('email')
newUsername = null
newEmail = null
specialMessage = ''
if not oldEmail
otherUser = yield User.findByEmail(oldUsername)
if otherUser
specialMessage = "Since you had no email set, we would have made your old username your new email.
But '#{oldUsername}' is already used by another account as an email by another account,
so instead we changed your username."
newUsername = str.slugify(oldUsername)
newEmail = ''
else
specialMessage = "Since you had no email set, we simply made your old username your new email instead."
newEmail = oldUsername
newUsername = ''
else if oldEmail is oldUsername
specialMessage = "Since your email and username are the same, we simply removed your username."
newUsername = ''
newEmail = oldEmail
else if not filter.test(oldEmail)
otherEmailUser = yield User.findByEmail(oldUsername)
otherUsernameUser = yield User.findByName(oldEmail)
if otherEmailUser
specialMessage = "Since your old email looks like a username and your old username looks like an email,
we would have swapped them on your account.
But '#{oldUsername}' is already used as an email by another account,
so instead we changed your username."
newUsername = str.slugify(oldUsername)
newEmail = oldEmail
else if otherUsernameUser
specialMessage = "Since your old email looks like a username and your old username looks like an email,
we would have swapped them on your account.
But '#{oldEmail}' is already used as a username by another account,
so instead we changed your username."
newUsername = str.slugify(oldUsername)
newEmail = oldEmail
else
specialMessage = "Since your old email looks like a username and your old username looks like an email,
we swapped them on your account."
newUsername = oldEmail
newEmail = oldUsername
else if oldUsername and oldEmail
# Since oldEmail passed the email filter,
specialMessage = "Since your old email is valid, we simply removed your username."
newUsername = ''
newEmail = oldEmail
else
console.log('unhandled user', user.toObject())
throw new Error('Unhandled user')
user.set({name: newUsername, email: newEmail})
console.log JSON.stringify({
oldUsername, oldEmail, newUsername, newEmail, specialMessage, _id: user.id
})
yield user.save()
content = changedUsernameTemplate({
oldUsername: oldUsername or '<i>(no username)</i>'
oldEmail: oldEmail or '<i>(no email)</i>'
newUsername: newUsername or '<i>(no username)</i>'
newEmail: newEmail or '<i>(no email)</i>'
specialMessage
})
context =
template: sendwithus.templates.plain_text_email
recipient:
address: oldUsername
sender:
address: 'team@codecombat.com'
name: 'CodeCombat Team'
template_data:
subject: 'Your Username Has Changed'
contentHTML: content
# Also send to the original email if it's valid
if filter.test(oldEmail)
context.cc = [
{ address: oldEmail }
]
yield sendwithus.api.sendAsync(context)
return 'Done'
if _.last(process.argv) is 'run'
database = require '../../server/commons/database'
mongoose = require 'mongoose'
### SET UP ###
do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.mixin _.str.exports()
GLOBAL.tv4 = require('tv4').tv4
database.connect()
co ->
yield exports.run()
process.exit()

View file

@ -10,6 +10,7 @@ Classroom = require '../models/Classroom'
languages = require '../routes/languages'
_ = require 'lodash'
errors = require '../commons/errors'
Promise = require 'bluebird'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
@ -270,6 +271,8 @@ UserSchema.statics.unconflictName = unconflictName = (name, done) ->
suffix = _.random(0, 9) + ''
unconflictName name + suffix, done
UserSchema.statics.unconflictNameAsync = Promise.promisify(unconflictName)
UserSchema.methods.sendWelcomeEmail = ->
return if not @get('email')
{ welcome_email_student, welcome_email_user } = sendwithus.templates
@ -361,9 +364,10 @@ UserSchema.pre('save', (next) ->
@set('email', undefined)
@set('emailLower', undefined)
if name = @get('name')
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
if filter.test(name)
return next(new errors.UnprocessableEntity('Name may not be an email'))
if not @allowEmailNames # for testing
filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990
if filter.test(name)
return next(new errors.UnprocessableEntity('Name may not be an email'))
@set('nameLower', name.toLowerCase())
else

View file

@ -122,4 +122,4 @@ module.exports.setup = (app) ->
app.put('/db/trial.request/:handle', mw.auth.checkHasPermission(['admin']), mw.trialRequests.put)
app.get('/db/trial.request/-/users', mw.auth.checkHasPermission(['admin']), mw.trialRequests.getUsers)
app.get('/healthcheck', mw.healthcheck)
app.get('/healthcheck', mw.healthcheck)

View file

@ -2,6 +2,7 @@ config = require '../server_config'
sendwithusAPI = require 'sendwithus'
swuAPIKey = config.mail.sendwithusAPIKey
log = require 'winston'
Promise = require 'bluebird'
module.exports.setupRoutes = (app) ->
return
@ -14,6 +15,8 @@ module.exports.api =
if swuAPIKey
module.exports.api = new sendwithusAPI swuAPIKey, debug
Promise.promisifyAll(module.exports.api)
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'

View file

@ -0,0 +1,115 @@
# Don't need to run this regularly, only running script once.
#utils = require '../utils'
#User = require '../../../server/models/User'
#request = require '../request'
#sendwithus = require '../../../server/sendwithus'
#fixEmailFormattedUsernames = require '../../../scripts/node/fixEmailFormattedUsernames'
#
#describe '/scripts/node/fixEmailFormattedUsernames', ->
#
# beforeEach utils.wrap (done) ->
# yield utils.clearModels([User])
# console.log('spy on send async')
# spyOn(sendwithus.api, 'sendAsync').and.callThrough()
# done()
#
# afterEach ->
# expect(sendwithus.api.sendAsync).toHaveBeenCalled()
#
# describe "when a user has no email set", ->
# beforeEach utils.wrap (done) ->
# @user = new User({name: 'an@email.com', points:100})
# @user.allowEmailNames = true
# yield @user.save()
# done()
#
# it 'moves the email-formatted username to be the user\'s email', utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('an@email.com')
# expect(user.get('name')).toBeUndefined()
# expect(user.get('points')).toBe(100) # make sure properties aren't removed
# done()
#
#
# describe "when another user exists with that email", ->
# beforeEach utils.wrap (done) ->
# @otherUser = new User({email: 'an@email.com'})
# yield @otherUser.save()
# done()
#
# it "slugifies the target user's username", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBeUndefined()
# expect(user.get('name')).toBe('anemailcom')
# done()
#
# describe "when a user has the same email and username", ->
# beforeEach utils.wrap (done) ->
# @user = new User({name: 'an@email.com', email: 'an@email.com'})
# @user.allowEmailNames = true
# yield @user.save()
# done()
#
# it "removes the user's username", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('an@email.com')
# expect(user.get('name')).toBeUndefined()
# done()
#
# describe "when the user has an email that isn't formatted like an email", ->
# beforeEach utils.wrap (done) ->
# @user = new User({name: 'an@email.com', email: 'a name'})
# @user.allowEmailNames = true
# yield @user.save()
# done()
#
# it "swaps the two", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('an@email.com')
# expect(user.get('name')).toBe('a name')
# done()
#
# describe "when another user already has the email-formatted name as an email", ->
# beforeEach utils.wrap (done) ->
# @otherUser = new User({email: 'an@email.com'})
# yield @otherUser.save()
# done()
#
# it "slugifies the target user's username", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('a name')
# expect(user.get('name')).toBe('anemailcom')
# done()
#
# describe "when another user already has the non-email-formatted email as a username", ->
# beforeEach utils.wrap (done) ->
# @otherUser = new User({name: 'a name'})
# yield @otherUser.save()
# done()
#
# it "slugifies the target user's username", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('a name')
# expect(user.get('name')).toBe('anemailcom')
# done()
#
# describe "when the user has a different but well formatted email set", ->
# beforeEach utils.wrap (done) ->
# @user = new User({name: 'an@email.com', email: 'another@email.com'})
# @user.allowEmailNames = true
# yield @user.save()
# done()
#
# it "removes the target user's username", utils.wrap (done) ->
# yield fixEmailFormattedUsernames.run()
# user = yield User.findById(@user.id)
# expect(user.get('email')).toBe('another@email.com')
# expect(user.get('name')).toBeUndefined()
# done()