async = require('async')
misc = require('smc-util/misc')
message = require('smc-util/message')
email = require('./email')
{defaults, required} = misc
{is_valid_password} = require('./create-account')
auth = require('./auth')
exports.forgot_password = (opts) ->
opts = defaults opts,
mesg : required
database : required
ip_address : required
cb : required
if opts.mesg.event != 'forgot_password'
opts.cb("Incorrect message event type: #{opts.mesg.event}")
return
if not misc.is_valid_email_address(opts.mesg.email_address)
opts.cb("Invalid email address.")
return
opts.mesg.email_address = misc.lower_email_address(opts.mesg.email_address)
id = null
async.series([
(cb) ->
opts.database.record_password_reset_attempt
email_address : opts.mesg.email_address
ip_address : opts.ip_address
cb : cb
(cb) ->
opts.database.count_password_reset_attempts
email_address : opts.mesg.email_address
age_s : 60*60
cb : (err, count) ->
if err
cb(err)
else if count >= 31
cb("Too many password resets for this email per hour; try again later.")
else
cb()
(cb) ->
opts.database.count_password_reset_attempts
ip_address : opts.ip_address
age_s : 60
cb : (err, count) ->
if err
cb(err)
else if count > 10
cb("Too many password resets per minute; try again later.")
else
cb()
(cb) ->
opts.database.count_password_reset_attempts
ip_address : opts.ip_address
age_s : 60*60
cb : (err, count) ->
if err
cb(err)
else if count > 60
cb("Too many password resets per hour; try again later.")
else
cb()
(cb) ->
opts.database.account_exists
email_address : opts.mesg.email_address
cb : (err, exists) ->
if err
cb(err)
else if not exists
cb("No account with e-mail address #{opts.mesg.email_address}")
else
cb()
(cb) ->
opts.database.set_password_reset
email_address : opts.mesg.email_address
ttl : 60*60
cb : (err, _id) ->
id = _id; cb(err)
(cb) ->
{DOMAIN_NAME, HELP_EMAIL, SITE_NAME} = require('smc-util/theme')
body = """
<div>Hello,</div>
<div> </div>
<div>
Somebody just requested to change the password of your #{SITE_NAME} account.
If you requested this password change, please click this link:</div>
<div> </div>
<div style="text-align: center;">
<span style="font-size:12px;"><b>
<a href="#{DOMAIN_NAME}/app#forgot-#{id}">#{DOMAIN_NAME}/app#forgot-#{id}</a>
</b></span>
</div>
<div> </div>
<div>If you don't want to change your password, ignore this message.</div>
<div> </div>
<div>In case of problems, email
<a href="mailto:#{HELP_EMAIL}">#{HELP_EMAIL}</a> immediately
(or just reply to this email).
<div> </div>
"""
email.send_email
subject : "#{SITE_NAME} Password Reset"
body : body
from : "CoCalc Help <#{HELP_EMAIL}>"
to : opts.mesg.email_address
category: "password_reset"
cb : cb
], opts.cb)
exports.reset_forgot_password = (opts) ->
opts = defaults opts,
mesg : required
database : required
cb : required
if opts.mesg.event != 'reset_forgot_password'
opts.cb("incorrect message event type: #{opts.mesg.event}")
return
email_address = account_id = db = null
async.series([
(cb) ->
[valid, reason] = is_valid_password(opts.mesg.new_password)
if not valid
cb(reason); return
opts.database.get_password_reset
id : opts.mesg.reset_code
cb : (err, x) ->
if err
cb(err)
else if not x
cb("Password reset request is no longer valid.")
else
email_address = x
cb()
(cb) ->
opts.database.get_account
email_address : email_address
columns : ['account_id']
cb : (err, account) ->
account_id = account?.account_id; cb(err)
(cb) ->
opts.database.change_password
account_id : account_id
password_hash : auth.password_hash(opts.mesg.new_password)
cb : (err, account) ->
if err
cb(err)
else
opts.database.delete_password_reset
id : opts.mesg.reset_code
cb : cb
], opts.cb)
exports.change_password = (opts) ->
opts = defaults opts,
mesg : required
account_id : required
database : required
ip_address : required
cb : required
account = null
opts.mesg.email_address = misc.lower_email_address(opts.mesg.email_address)
async.series([
(cb) ->
if not opts.mesg.email_address?
cb("email_address must be specified")
return
opts.database.get_account
email_address : opts.mesg.email_address
columns : ['password_hash', 'account_id']
cb : (error, result) ->
if error
cb({other:error})
return
if result.account_id != opts.account_id
cb({other:'invalid account_id'})
return
account = result
auth.is_password_correct
database : opts.database
account_id : result.account_id
password : opts.mesg.old_password
password_hash : account.password_hash
allow_empty_password : true
cb : (err, is_correct) ->
if err
cb(err)
else
if not is_correct
err = "invalid old password"
opts.database.log
event : 'change_password'
value : {email_address:opts.mesg.email_address, client_ip_address:opts.ip_address, message:err}
cb(err)
else
cb()
(cb) ->
[valid, reason] = is_valid_password(opts.mesg.new_password)
if not valid
cb({new_password:reason})
else
cb()
(cb) ->
opts.database.log
event : "change_password"
value :
account_id : account.account_id
client_ip_address : opts.ip_address
previous_password_hash : account.password_hash
opts.database.change_password
account_id : account.account_id
password_hash : auth.password_hash(opts.mesg.new_password),
cb : cb
], opts.cb)
exports.change_email_address = (opts) ->
opts = defaults opts,
mesg : required
database : required
account_id : required
ip_address : required
logger : undefined
cb : required
if opts.logger?
dbg = (m...) -> opts.logger?.debug("change_email_address(#{opts.mesg.account_id}): ", m...)
dbg()
else
dbg = ->
opts.mesg.new_email_address = misc.lower_email_address(opts.mesg.new_email_address)
if not misc.is_valid_email_address(opts.mesg.new_email_address)
dbg("invalid email address")
opts.cb('email_invalid')
return
if opts.mesg.account_id != opts.account_id
opts.cb("account_id in mesg is not what user is signed in as")
return
async.series([
(cb) ->
auth.is_password_correct
database : opts.database
account_id : opts.mesg.account_id
password : opts.mesg.password
allow_empty_password : true
cb : (err, is_correct) ->
if err
cb("Error checking password -- please try again in a minute -- #{err}.")
else if not is_correct
cb("invalid_password")
else
cb()
(cb) ->
dbg("log change to db")
opts.database.log
event : 'change_email_address'
value :
client_ip_address : opts.ip_address
new_email_address : opts.mesg.new_email_address
dbg("actually make change in db")
opts.database.change_email_address
account_id : opts.mesg.account_id
email_address : opts.mesg.new_email_address
cb : cb
(cb) ->
opts.database.do_account_creation_actions
email_address : opts.mesg.new_email_address
account_id : opts.mesg.account_id
cb : cb
], opts.cb)