{EventEmitter} = require('events')
uuid = require('node-uuid')
async = require('async')
Cookies = require('cookies')
misc = require('smc-util/misc')
{defaults, required, to_safe_str} = misc
{JSON_CHANNEL} = require('smc-util/client')
message = require('smc-util/message')
base_url_lib = require('./base-url')
access = require('./access')
clients = require('./clients').get_clients()
auth = require('./auth')
auth_token = require('./auth-token')
password = require('./password')
local_hub_connection = require('./local_hub_connection')
sign_in = require('./sign-in')
smc_version = require('./hub-version')
hub_projects = require('./projects')
{get_stripe} = require('./stripe/connect')
{get_support} = require('./support')
{send_email} = require('./email')
{api_key_action} = require('./api/manage')
{create_account, delete_account} = require('./create-account')
underscore = require('underscore')
DEBUG2 = !!process.env.SMC_DEBUG2
REQUIRE_ACCOUNT_TO_EXECUTE_CODE = false
MESG_QUEUE_INTERVAL_MS = 0
MESG_QUEUE_MAX_COUNT = 300
MESG_QUEUE_MAX_WARN = 50
MESG_QUEUE_MAX_SIZE_MB = 10
CACHE_PROJECT_AUTH_MS = 1000*60*15
CLIENT_DESTROY_TIMER_S = 60*10
CLIENT_MIN_ACTIVE_S = 45
MetricsRecorder = require('./metrics-recorder')
mesg_from_client_total = MetricsRecorder.new_counter('mesg_from_client_total',
'counts Client::handle_json_message_from_client invocations', ['type', 'event'])
push_to_client_stats_h = MetricsRecorder.new_histogram('push_to_client_histo_ms', 'Client: push_to_client',
buckets : [1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000]
labels: ['event']
)
push_to_client_stats_q = MetricsRecorder.new_quantile('push_to_client_quant_ms', 'Client: push_to_client',
percentiles : [0, 0.25, 0.5, 0.75, 0.9, 0.99, 1]
labels: ['event']
)
push_to_client_to_json_summary = MetricsRecorder.new_summary('push_to_client_to_json',
'summary stats for Client::push_to_client/to_json', labels: ['event'])
uncaught_exception_total = MetricsRecorder.new_counter('uncaught_exception_total', 'counts "BUG"s')
class exports.Client extends EventEmitter
constructor: (opts) ->
@_opts = defaults opts,
conn : undefined
logger : undefined
database : required
compute_server : required
host : undefined
port : undefined
@conn = @_opts.conn
@logger = @_opts.logger
@database = @_opts.database
@compute_server = @_opts.compute_server
@_when_connected = new Date()
@_messages =
being_handled : {}
total_time : 0
count : 0
@account_id = undefined
if @conn?
@init_conn()
else
@id = misc.uuid()
init_conn: =>
@_data_handlers = {}
@_data_handlers[JSON_CHANNEL] = @handle_json_message_from_client
@compute_session_uuids = []
@install_conn_handlers()
@ip_address = @conn.address.ip
@id = @conn.id
@cookies = {}
c = new Cookies(@conn.request)
@_remember_me_value = c.get(base_url_lib.base_url() + 'remember_me')
@check_for_remember_me()
@_remember_me_interval = setInterval(@check_for_remember_me, 1000*60*5)
touch: (opts={}) =>
if not @account_id
opts.cb?('not logged in')
return
opts = defaults opts,
project_id : undefined
path : undefined
action : 'edit'
force : false
cb : undefined
if not @_touch_lock?
@_touch_lock = {}
key = "#{opts.project_id}-#{opts.path}-#{opts.action}"
if not opts.force and @_touch_lock[key]
opts.cb?("touch lock")
return
opts.account_id = @account_id
@_touch_lock[key] = true
delete opts.force
@database.touch(opts)
setTimeout((()=>delete @_touch_lock[key]), CLIENT_MIN_ACTIVE_S*1000)
install_conn_handlers: () =>
dbg = @dbg('install_conn_handlers')
if @_destroy_timer?
clearTimeout(@_destroy_timer)
delete @_destroy_timer
@conn.on "data", (data) =>
@handle_data_from_client(data)
@conn.on "end", () =>
dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address}) -- CLOSED")
@query_cancel_all_changefeeds()
@_destroy_timer = setTimeout(@destroy, 1000*CLIENT_DESTROY_TIMER_S)
dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address}) ESTABLISHED")
dbg: (desc) =>
if @logger?.debug
return (m...) => @logger.debug("Client(#{@id}).#{desc}: #{JSON.stringify(m...)}")
else
return ->
destroy: () =>
dbg = @dbg('destroy')
dbg("destroy connection: hub <--> client(id=#{@id}, address=#{@ip_address}) -- CLOSED")
clearInterval(@_remember_me_interval)
@query_cancel_all_changefeeds()
@closed = true
@emit('close')
@compute_session_uuids = []
c = clients[@id]
delete clients[@id]
if c? and c.call_callbacks?
for id,f of c.call_callbacks
f("connection closed")
delete c.call_callbacks
for h in local_hub_connection.all_local_hubs()
h.free_resources_for_client_id(@id)
remember_me_failed: (reason) =>
return if not @conn?
@signed_out()
@push_to_client(message.remember_me_failed(reason:reason))
check_for_remember_me: () =>
return if not @conn?
dbg = @dbg("check_for_remember_me")
value = @_remember_me_value
if not value?
@remember_me_failed("no remember_me cookie")
return
x = value.split('$')
if x.length != 4
@remember_me_failed("invalid remember_me cookie")
return
hash = auth.generate_hash(x[0], x[1], x[2], x[3])
dbg("checking for remember_me cookie with hash='#{hash.slice(0,15)}...'")
@database.get_remember_me
hash : hash
cb : (error, signed_in_mesg) =>
dbg("remember_me: got error", error, "signed_in_mesg", signed_in_mesg)
if error
@remember_me_failed("error accessing database")
return
if not signed_in_mesg?
@remember_me_failed("remember_me deleted or expired")
return
if @account_id != signed_in_mesg.account_id
signed_in_mesg.hub = @_opts.host + ':' + @_opts.port
@hash_session_id = hash
@signed_in(signed_in_mesg)
@push_to_client(signed_in_mesg)
cap_session_limits: (limits) ->
if @account_id?
misc.min_object(limits, SESSION_LIMITS)
else
misc.min_object(limits, SESSION_LIMITS_NOT_LOGGED_IN)
push_to_client: (mesg, cb) =>
if @closed
cb?("disconnected")
return
dbg = @dbg("push_to_client")
if mesg.event != 'pong'
dbg("hub --> client (client=#{@id}): #{misc.trunc(to_safe_str(mesg),300)}")
if mesg.id?
start = @_messages.being_handled[mesg.id]
if start?
time_taken = new Date() - start
delete @_messages.being_handled[mesg.id]
@_messages.total_time += time_taken
@_messages.count += 1
avg = Math.round(@_messages.total_time / @_messages.count)
dbg("[#{time_taken} mesg_time_ms] [#{avg} mesg_avg_ms] -- mesg.id=#{mesg.id}")
push_to_client_stats_q.observe({event:mesg.event}, time_taken)
push_to_client_stats_h.observe({event:mesg.event}, time_taken)
listen = cb? and not mesg.id?
if listen
mesg.id = misc.uuid()
if not @call_callbacks?
@call_callbacks = {}
@call_callbacks[mesg.id] = cb
f = () =>
g = @call_callbacks?[mesg.id]
if g?
delete @call_callbacks[mesg.id]
g("timed out")
setTimeout(f, 15000)
t = new Date()
json = misc.to_json_socket(mesg)
tm = new Date() - t
if tm > 10
dbg("mesg.id=#{mesg.id}: time to json=#{tm}ms; length=#{json.length}; value='#{misc.trunc(json, 500)}'")
@push_data_to_client(JSON_CHANNEL, json)
if not listen
cb?()
return
push_data_to_client: (channel, data) ->
return if not @conn?
if @closed
return
@conn.write(channel + data)
error_to_client: (opts) ->
opts = defaults opts,
id : undefined
error : required
@push_to_client(message.error(id:opts.id, error:opts.error))
success_to_client: (opts) ->
opts = defaults opts,
id : required
@push_to_client(message.success(id:opts.id))
signed_in: (signed_in_mesg) =>
return if not @conn?
@signed_in_mesg = signed_in_mesg
@account_id = signed_in_mesg.account_id
sign_in.record_sign_in
ip_address : @ip_address
successful : true
remember_me : signed_in_mesg.remember_me
email_address : signed_in_mesg.email_address
account_id : signed_in_mesg.account_id
database : @database
@get_groups()
signed_out: () =>
@account_id = undefined
get_cookie: (opts) ->
opts = defaults opts,
name : required
cb : required
if not @conn?.id?
return
@once("get_cookie-#{opts.name}", (value) -> opts.cb(value))
@push_to_client(message.cookies(id:@conn.id, get:opts.name, url:base_url_lib.base_url()+"/cookies"))
set_cookie: (opts) ->
opts = defaults opts,
name : required
value : required
ttl : undefined
if not @conn?.id?
return
options = {}
if opts.ttl?
options.expires = new Date(new Date().getTime() + 1000*opts.ttl)
@cookies[opts.name] = {value:opts.value, options:options}
@push_to_client(message.cookies(id:@conn.id, set:opts.name, url:base_url_lib.base_url()+"/cookies", value:opts.value))
remember_me: (opts) ->
return if not @conn?
opts = defaults opts,
email_address : required
account_id : required
ttl : 24*3600 *30
cb : undefined
ttl = opts.ttl; delete opts.ttl
opts.hub = @_opts.host
opts.remember_me = true
opts0 = misc.copy(opts)
delete opts0.cb
signed_in_mesg = message.signed_in(opts0)
session_id = uuid.v4()
@hash_session_id = auth.password_hash(session_id)
x = @hash_session_id.split('$')
@_remember_me_value = [x[0], x[1], x[2], session_id].join('$')
@set_cookie
name : base_url_lib.base_url() + 'remember_me'
value : @_remember_me_value
ttl : ttl
@database.save_remember_me
account_id : opts.account_id
hash : @hash_session_id
value : signed_in_mesg
ttl : ttl
cb : opts.cb
invalidate_remember_me: (opts) ->
return if not @conn?
opts = defaults opts,
cb : required
if @hash_session_id?
@database.delete_remember_me
hash : @hash_session_id
cb : opts.cb
else
opts.cb()
handle_data_from_client: (data) =>
return if not @conn?
dbg = @dbg("handle_data_from_client")
if DEBUG2
dbg("handle_data_from_client('#{misc.trunc(data.toString(),400)}')")
if data.length >= MESG_QUEUE_MAX_SIZE_MB * 10000000
msg = "The server ignored a huge message since it exceeded the allowed size limit of #{MESG_QUEUE_MAX_SIZE_MB}MB. Please report what caused this if you can."
@logger?.error(msg)
@error_to_client(error:msg)
return
if data.length == 0
msg = "The server ignored a message since it was empty."
@logger?.error(msg)
@error_to_client(error:msg)
return
if not @_handle_data_queue?
@_handle_data_queue = []
channel = data[0]
h = @_data_handlers[channel]
if not h?
if channel != 'X'
@logger?.error("unable to handle data on an unknown channel: '#{channel}', '#{data}'")
@push_to_client( message.session_reconnect(data_channel : channel) )
return
@_handle_data_queue.push([h, data.slice(1)])
if @_handle_data_queue_empty_function?
return
@_handle_data_queue_empty_function = () =>
if @_handle_data_queue.length == 0
delete @_handle_data_queue_empty_function
return
if @_handle_data_queue.length > MESG_QUEUE_MAX_WARN
dbg("MESG_QUEUE_MAX_WARN(=#{MESG_QUEUE_MAX_WARN}) exceeded (=#{@_handle_data_queue.length}) -- just a warning")
if @_handle_data_queue.length > MESG_QUEUE_MAX_COUNT
dbg("MESG_QUEUE_MAX_COUNT(=#{MESG_QUEUE_MAX_COUNT}) exceeded (=#{@_handle_data_queue.length}) -- drop oldest messages")
while @_handle_data_queue.length > MESG_QUEUE_MAX_COUNT
discarded_mesg = @_handle_data_queue.shift()
data = discarded_mesg?[1]
dbg("discarded_mesg='#{misc.trunc(data?.toString?(),1000)}'")
task = @_handle_data_queue.shift()
task[0](task[1])
setTimeout( @_handle_data_queue_empty_function, MESG_QUEUE_INTERVAL_MS )
@_handle_data_queue_empty_function()
register_data_handler: (h) ->
return if not @conn?
if not @_last_channel?
@_last_channel = 1
while true
@_last_channel += 1
channel = String.fromCharCode(@_last_channel)
if not @_data_handlers[channel]?
break
@_data_handlers[channel] = h
return channel
handle_json_message_from_client: (data) =>
return if not @conn?
if @_ignore_client
return
try
mesg = misc.from_json_socket(data)
catch error
@logger?.error("error parsing incoming mesg (invalid JSON): #{mesg}")
return
dbg = @dbg('handle_json_message_from_client')
if mesg.event != 'ping'
dbg("hub <-- client: #{misc.trunc(to_safe_str(mesg), 120)}")
if @call_callbacks? and mesg.id?
f = @call_callbacks[mesg.id]
if f?
delete @call_callbacks[mesg.id]
f(undefined, mesg)
return
if mesg.id?
@_messages.being_handled[mesg.id] = new Date()
handler = @["mesg_#{mesg.event}"]
if handler?
handler(mesg)
else
@push_to_client(message.error(error:"Hub does not know how to handle a '#{mesg.event}' event.", id:mesg.id))
if mesg.event == 'get_all_activity'
dbg("ignoring all further messages from old client=#{@id}")
@_ignore_client = true
mesg_ping: (mesg) =>
@push_to_client(message.pong(id:mesg.id, now:new Date()))
mesg_start_session: (mesg) =>
if REQUIRE_ACCOUNT_TO_EXECUTE_CODE and not @account_id?
@push_to_client(message.error(id:mesg.id, error:"You must be signed in to start a session."))
return
switch mesg.type
when 'console'
@connect_to_console_session(mesg)
else
@error_to_client(id:mesg.id, error:"Unknown message type '#{mesg.type}'")
mesg_connect_to_session: (mesg) =>
if REQUIRE_ACCOUNT_TO_EXECUTE_CODE and not @account_id?
@push_to_client(message.error(id:mesg.id, error:"You must be signed in to start a session."))
return
switch mesg.type
when 'console'
if not mesg.params?.path? or not mesg.params?.filename?
@push_to_client(message.error(id:mesg.id, error:"console session path and filename must be defined"))
return
@connect_to_console_session(mesg)
else
@push_to_client(message.error(id:mesg.id, error:"Connecting to session of type '#{mesg.type}' not yet implemented"))
connect_to_console_session: (mesg) =>
@get_project mesg, 'write', (err, project) =>
if not err
project.console_session
client : @
params : mesg.params
session_uuid : mesg.session_uuid
cb : (err, connect_mesg) =>
if err
@error_to_client(id:mesg.id, error:err)
else
connect_mesg.id = mesg.id
@push_to_client(connect_mesg)
mesg_terminate_session: (mesg) =>
@get_project mesg, 'write', (err, project) =>
if not err
project.terminate_session
session_uuid : mesg.session_uuid
cb : (err, resp) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(mesg)
mesg_create_account: (mesg) =>
create_account
client : @
mesg : mesg
database : @database
logger : @logger
host : @_opts.host
port : @_opts.port
sign_in : @conn?
mesg_delete_account: (mesg) =>
delete_account
client : @
mesg : mesg
database : @database
logger : @logger
mesg_sign_in: (mesg) =>
sign_in.sign_in
client : @
mesg : mesg
logger : @logger
database : @database
host : @_opts.host
port : @_opts.port
mesg_sign_in_using_auth_token: (mesg) =>
sign_in.sign_in_using_auth_token
client : @
mesg : mesg
logger : @logger
database : @database
host : @_opts.host
port : @_opts.port
mesg_sign_out: (mesg) =>
if not @account_id?
@push_to_client(message.error(id:mesg.id, error:"Not signed in."))
return
if mesg.everywhere
@database.invalidate_all_remember_me
account_id : @account_id
@signed_out()
@invalidate_remember_me
cb:(error) =>
@dbg('mesg_sign_out')("signing out: #{mesg.id}, #{error}")
if not error
@push_to_client(message.error(id:mesg.id, error:error))
else
@push_to_client(message.signed_out(id:mesg.id))
mesg_change_password: (mesg) =>
password.change_password
mesg : mesg
account_id : @account_id
ip_address : @ip_address
database : @database
cb : (err) =>
@push_to_client(message.changed_password(id:mesg.id, error:err))
mesg_forgot_password: (mesg) =>
password.forgot_password
mesg : mesg
ip_address : @ip_address
database : @database
cb : (err) =>
@push_to_client(message.forgot_password_response(id:mesg.id, error:err))
mesg_reset_forgot_password: (mesg) =>
password.reset_forgot_password
mesg : mesg
database : @database
cb : (err) =>
@push_to_client(message.reset_forgot_password_response(id:mesg.id, error:err))
mesg_change_email_address: (mesg) =>
password.change_email_address
mesg : mesg
account_id : @account_id
ip_address : @ip_address
database : @database
logger : @logger
cb : (err) =>
@push_to_client(message.changed_email_address(id:mesg.id, error:err))
mesg_unlink_passport: (mesg) =>
if not @account_id?
@error_to_client(id:mesg.id, error:"must be logged in")
else
@database.delete_passport
account_id : @account_id
strategy : mesg.strategy
id : mesg.id
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
get_groups: (cb) =>
if @groups?
cb?(undefined, @groups)
return
@database.get_account
columns : ['groups']
account_id : @account_id
cb : (err, r) =>
if err
cb?(err)
else
@groups = r['groups']
cb?(undefined, @groups)
mesg_log_client_error: (mesg) =>
@dbg('mesg_log_client_error')(mesg.error)
if not mesg.type?
mesg.type = "error"
if not mesg.error?
mesg.error = "error"
@database.log_client_error
event : mesg.type
error : mesg.error
account_id : @account_id
cb : (err) =>
if not mesg.id?
return
if err
@error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
mesg_webapp_error: (mesg) =>
@dbg('mesg_webapp_error')(mesg.msg)
mesg = misc.copy_without(mesg, 'event')
mesg.account_id = @account_id
@database.webapp_error(mesg)
get_project: (mesg, permission, cb) =>
dbg = @dbg('get_project')
err = undefined
if not mesg.project_id?
err = "mesg must have project_id attribute -- #{to_safe_str(mesg)}"
else if not @account_id?
err = "user must be signed in before accessing projects"
if err
if mesg.id?
@error_to_client(id:mesg.id, error:err)
cb(err)
return
key = mesg.project_id + permission
project = @_project_cache?[key]
if project?
cb(undefined, project)
return
dbg()
async.series([
(cb) =>
switch permission
when 'read'
access.user_has_read_access_to_project
project_id : mesg.project_id
account_id : @account_id
account_groups : @groups
database : @database
cb : (err, result) =>
if err
cb("Internal error determining user permission -- #{err}")
else if not result
cb("User #{@account_id} does not have read access to project #{mesg.project_id}")
else
cb()
when 'write'
access.user_has_write_access_to_project
database : @database
project_id : mesg.project_id
account_groups : @groups
account_id : @account_id
cb : (err, result) =>
if err
cb("Internal error determining user permission -- #{err}")
else if not result
cb("User #{@account_id} does not have write access to project #{mesg.project_id}")
else
cb()
else
cb("Internal error -- unknown permission type '#{permission}'")
], (err) =>
if err
if mesg.id?
@error_to_client(id:mesg.id, error:err)
dbg("error -- #{err}")
cb(err)
else
project = hub_projects.new_project(mesg.project_id, @database, @compute_server)
@database.touch_project(project_id:mesg.project_id)
if not @_project_cache?
@_project_cache = {}
@_project_cache[key] = project
setTimeout((()=>delete @_project_cache[key]), CACHE_PROJECT_AUTH_MS)
dbg("got project; caching and returning")
cb(undefined, project)
)
mesg_create_project: (mesg) =>
if not @account_id?
@error_to_client(id: mesg.id, error: "You must be signed in to create a new project.")
return
@touch()
dbg = @dbg('mesg_create_project')
project_id = undefined
project = undefined
location = undefined
async.series([
(cb) =>
dbg("create project entry in database")
@database.create_project
account_id : @account_id
title : mesg.title
description : mesg.description
cb : (err, _project_id) =>
project_id = _project_id; cb(err)
(cb) =>
dbg("open project...")
@compute_server.project
project_id : project_id
cb : (err, project) =>
if err
dbg("failed to get project -- #{err}")
else
async.series([
(cb) =>
project.open(cb:cb)
(cb) =>
project.state(cb:cb, force:true, update:true)
(cb) =>
if mesg.start
project.start(cb:cb)
else
dbg("not auto-starting the new project")
cb()
], (err) =>
dbg("open project and get state: #{err}")
)
cb()
], (err) =>
if err
dbg("error; project #{project_id} -- #{err}")
@error_to_client(id: mesg.id, error: "Failed to create new project '#{mesg.title}' -- #{misc.to_json(err)}")
else
dbg("SUCCESS: project #{project_id}")
@push_to_client(message.project_created(id:mesg.id, project_id:project_id))
dbg("start process of opening project")
@get_project {project_id:project_id}, 'write', (err, project) =>
)
mesg_write_text_file_to_project: (mesg) =>
@get_project mesg, 'write', (err, project) =>
if err
return
project.write_file
path : mesg.path
data : mesg.content
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.file_written_to_project(id:mesg.id))
mesg_read_text_file_from_project: (mesg) =>
@get_project mesg, 'read', (err, project) =>
if err
return
project.read_file
path : mesg.path
cb : (err, content) =>
if err
@error_to_client(id:mesg.id, error:err)
else
t = content.blob.toString()
@push_to_client(message.text_file_read_from_project(id:mesg.id, content:t))
mesg_project_exec: (mesg) =>
if mesg.command == "ipython-notebook"
return
@get_project mesg, 'write', (err, project) =>
if err
return
project.call
mesg : mesg
timeout : mesg.timeout
cb : (err, resp) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(resp)
mesg_copy_path_between_projects: (mesg) =>
@touch()
if not mesg.src_project_id?
@error_to_client(id:mesg.id, error:"src_project_id must be defined")
return
if not mesg.target_project_id?
@error_to_client(id:mesg.id, error:"target_project_id must be defined")
return
if not mesg.src_path?
@error_to_client(id:mesg.id, error:"src_path must be defined")
return
async.series([
(cb) =>
async.parallel([
(cb) =>
access.user_has_read_access_to_project
project_id : mesg.src_project_id
account_id : @account_id
account_groups : @groups
database : @database
cb : (err, result) =>
if err
cb(err)
else if not result
cb("user must have read access to source project #{mesg.src_project_id}")
else
cb()
(cb) =>
access.user_has_write_access_to_project
database : @database
project_id : mesg.target_project_id
account_id : @account_id
account_groups : @groups
cb : (err, result) =>
if err
cb(err)
else if not result
cb("user must have write access to target project #{mesg.target_project_id}")
else
cb()
], cb)
(cb) =>
@compute_server.project
project_id : mesg.src_project_id
cb : (err, project) =>
if err
cb(err); return
else
project.copy_path
path : mesg.src_path
target_project_id : mesg.target_project_id
target_path : mesg.target_path
overwrite_newer : mesg.overwrite_newer
delete_missing : mesg.delete_missing
backup : mesg.backup
timeout : mesg.timeout
exclude_history : mesg.exclude_history
cb : cb
], (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))
)
mesg_local_hub: (mesg) =>
dbg = @dbg('mesg_local_hub')
dbg("hub --> local_hub: ", mesg)
@get_project mesg, 'write', (err, project) =>
if err
return
if not mesg.message?
@error_to_client(id:mesg.id, error:"message must be defined")
return
if mesg.message.event == 'project_exec' and mesg.message.command == "ipython-notebook"
return
mesg.message.client_id = @id
project.call
mesg : mesg.message
timeout : mesg.timeout
multi_response : mesg.multi_response
cb : (err, resp) =>
if err
dbg("ERROR: #{err} calling message #{misc.to_json(mesg.message)}")
@error_to_client(id:mesg.id, error:err)
else
if not mesg.multi_response
resp.id = mesg.id
@push_to_client(resp)
mesg_user_search: (mesg) =>
if not mesg.limit? or mesg.limit > 50
mesg.limit = 50
@touch()
@database.user_search
query : mesg.query
limit : mesg.limit
cb : (err, results) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.user_search_results(id:mesg.id, results:results))
mesg_invite_collaborator: (mesg) =>
@touch()
@get_project mesg, 'write', (err, project) =>
if err
return
@database.add_user_to_project
project_id : mesg.project_id
account_id : mesg.account_id
group : 'collaborator'
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))
mesg_invite_noncloud_collaborators: (mesg) =>
dbg = @dbg('mesg_invite_noncloud_collaborators')
@touch()
@get_project mesg, 'write', (err, project) =>
if err
return
if mesg.to.length > 1024
@error_to_client(id:mesg.id, error:"Specify less recipients when adding collaborators to project.")
return
to = (x for x in mesg.to.replace(/\s/g,",").replace(/;/g,",").split(',') when x)
email = mesg.email
invite_user = (email_address, cb) =>
dbg("inviting #{email_address}")
if not misc.is_valid_email_address(email_address)
cb("invalid email address '#{email_address}'")
return
email_address = misc.lower_email_address(email_address)
if email_address.length >= 128
cb("email address must be at most 128 characters: '#{email_address}'")
return
done = false
account_id = undefined
async.series([
(cb) =>
@database.account_exists
email_address : email_address
cb : (err, _account_id) =>
dbg("account_exists: #{err}, #{_account_id}")
account_id = _account_id
cb(err)
(cb) =>
if account_id
dbg("user #{email_address} already has an account -- add directly")
done = true
@database.add_user_to_project
project_id : mesg.project_id
account_id : account_id
group : 'collaborator'
cb : cb
else
dbg("user #{email_address} doesn't have an account yet -- may send email (if we haven't recently)")
@database.account_creation_actions
email_address : email_address
action : {action:'add_to_project', group:'collaborator', project_id:mesg.project_id}
ttl : 60*60*24*14
cb : cb
(cb) =>
if done
cb()
else
@database.when_sent_project_invite
project_id : mesg.project_id
to : email_address
cb : (err, when_sent) =>
if err
cb(err)
else if when_sent - 0 >= misc.days_ago(7) - 0
done = true
cb()
else
cb()
(cb) =>
if done
cb()
else
cb()
subject = "CoCalc Invitation"
if mesg.subject?
subject = mesg.subject
if mesg.link2proj?
base_url = mesg.link2proj.split("/")
base_url = "#{base_url[0]}//#{base_url[2]}"
direct_link = "Then go to <a href='#{mesg.link2proj}'>the project '#{mesg.title}'</a>."
else
base_url = 'https://cocalc.com/'
direct_link = ''
opts =
to : email_address
bcc : '[email protected]'
fromname : 'CoCalc'
from : '[email protected]'
replyto : mesg.replyto ? '[email protected]'
replyto_name : mesg.replyto_name
subject : subject
category : "invite"
asm_group : 699
body : email + """<br/><br/>
<b>To accept the invitation, please sign up at
<a href='#{base_url}'>#{base_url}</a>
using exactly the email address '#{email_address}'.
#{direct_link}</b><br/>"""
cb : (err) =>
if err
dbg("FAILED to send email to #{email_address} -- err={misc.to_json(err)}")
@database.sent_project_invite
project_id : mesg.project_id
to : email_address
error : err
send_email(opts)
], cb)
async.map to, invite_user, (err, results) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.invite_noncloud_collaborators_resp(id:mesg.id, mesg:"Invited #{mesg.to} to collaborate on a project."))
mesg_remove_collaborator: (mesg) =>
@touch()
@get_project mesg, 'write', (err, project) =>
if err
return
@database.remove_collaborator_from_project
project_id : mesg.project_id
account_id : mesg.account_id
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))
mesg_remove_blob_ttls: (mesg) =>
if not @account_id?
@push_to_client(message.error(id:mesg.id, error:"not yet signed in"))
else
@database.remove_blob_ttls
uuids : mesg.uuids
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))
mesg_version: (mesg) =>
@smc_version = mesg.version
@dbg('mesg_version')("client.smc_version=#{mesg.version}")
if mesg.version < smc_version.version
@push_version_update()
push_version_update: =>
@push_to_client(message.version(version:smc_version.version, min_version:smc_version.min_browser_version))
if smc_version.min_browser_version and @smc_version and @smc_version < smc_version.min_browser_version
if new Date() - @_when_connected <= 30000
@conn.end()
else
setTimeout((()=>@conn.end()), 60000)
user_is_in_group: (group) =>
return @groups? and group in @groups
mesg_project_set_quotas: (mesg) =>
if not @user_is_in_group('admin')
@error_to_client(id:mesg.id, error:"must be logged in and a member of the admin group to set project quotas")
else if not misc.is_valid_uuid_string(mesg.project_id)
@error_to_client(id:mesg.id, error:"invalid project_id")
else
project = undefined
dbg = @dbg("mesg_project_set_quotas(project_id='#{mesg.project_id}')")
async.series([
(cb) =>
dbg("update base quotas in the database")
@database.set_project_settings
project_id : mesg.project_id
settings : misc.copy_without(mesg, ['event', 'id'])
cb : cb
(cb) =>
dbg("get project from compute server")
@compute_server.project
project_id : mesg.project_id
cb : (err, p) =>
project = p; cb(err)
(cb) =>
dbg("determine total quotas and apply")
project.set_all_quotas(cb:cb)
], (err) =>
if err
@error_to_client(id:mesg.id, error:"problem setting project quota -- #{err}")
else
@push_to_client(message.success(id:mesg.id))
)
path_is_in_public_paths: (path, paths) =>
return misc.path_is_in_public_paths(path, misc.keys(paths))
get_public_project: (opts) =>
opts = defaults opts,
project_id : undefined
path : undefined
use_cache : true
cb : required
if not opts.project_id?
opts.cb("get_public_project: project_id must be defined")
return
if not opts.path?
opts.cb("get_public_project: path must be defined")
return
@database.path_is_public
project_id : opts.project_id
path : opts.path
cb : (err, is_public) =>
if err
opts.cb(err)
return
if is_public
@compute_server.project
project_id : opts.project_id
cb : opts.cb
else
opts.cb("path '#{opts.path}' of project with id '#{opts.project_id}' is not public")
mesg_public_get_directory_listing: (mesg) =>
dbg = @dbg('mesg_public_get_directory_listing')
for k in ['path', 'project_id']
if not mesg[k]?
dbg("missing stuff in message")
@error_to_client(id:mesg.id, error:"must specify #{k}")
return
project = undefined
listing = undefined
async.series([
(cb) =>
dbg("checking for public path")
@database.has_public_path
project_id : mesg.project_id
cb : (err, is_public) =>
if err
dbg("error checking -- #{err}")
cb(err)
else if not is_public
dbg("no public paths at all -- deny all listings")
cb("not_public")
else
cb()
(cb) =>
dbg("get the project")
@compute_server.project
project_id : mesg.project_id
cb : (err, x) =>
project = x; cb(err)
(cb) =>
dbg("get the directory listing")
project.directory_listing
path : mesg.path
hidden : mesg.hidden
time : mesg.time
start : mesg.start
limit : mesg.limit
cb : (err, x) =>
listing = x; cb(err)
(cb) =>
dbg("filtering out public paths from listing")
@database.filter_public_paths
project_id : mesg.project_id
path : mesg.path
listing : listing
cb : (err, x) =>
listing = x; cb(err)
], (err) =>
if err
dbg("something went wrong -- #{err}")
@error_to_client(id:mesg.id, error:err)
else
dbg("it worked; telling client")
@push_to_client(message.public_directory_listing(id:mesg.id, result:listing))
)
mesg_public_get_text_file: (mesg) =>
if not mesg.path?
@error_to_client(id:mesg.id, error:'must specify path')
return
@get_public_project
project_id : mesg.project_id
path : mesg.path
cb : (err, project) =>
if err
@error_to_client(id:mesg.id, error:err)
return
project.read_file
path : mesg.path
maxsize : 20000000
cb : (err, data) =>
if err
@error_to_client(id:mesg.id, error:err)
else
if Buffer.isBuffer(data)
data = data.toString('utf-8')
@push_to_client(message.public_text_file_contents(id:mesg.id, data:data))
mesg_copy_public_path_between_projects: (mesg) =>
@touch()
if not mesg.src_project_id?
@error_to_client(id:mesg.id, error:"src_project_id must be defined")
return
if not mesg.target_project_id?
@error_to_client(id:mesg.id, error:"target_project_id must be defined")
return
if not mesg.src_path?
@error_to_client(id:mesg.id, error:"src_path must be defined")
return
project = undefined
async.series([
(cb) =>
access.user_has_write_access_to_project
database : @database
project_id : mesg.target_project_id
account_id : @account_id
account_groups : @groups
cb : (err, result) =>
if err
cb(err)
else if not result
cb("user must have write access to target project #{mesg.target_project_id}")
else
cb()
(cb) =>
@get_public_project
project_id : mesg.src_project_id
path : mesg.src_path
cb : (err, x) =>
project = x
cb(err)
(cb) =>
project.copy_path
path : mesg.src_path
target_project_id : mesg.target_project_id
target_path : mesg.target_path
overwrite_newer : mesg.overwrite_newer
delete_missing : mesg.delete_missing
timeout : mesg.timeout
exclude_history : mesg.exclude_history
backup : mesg.backup
cb : cb
], (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))
)
mesg_query: (mesg) =>
query = mesg.query
if not query?
@error_to_client(id:mesg.id, error:"malformed query")
return
dbg = @dbg("user_query")
first = true
if mesg.changes
@_query_changefeeds ?= {}
@_query_changefeeds[mesg.id] = true
mesg_id = mesg.id
@database.user_query
account_id : @account_id
query : query
options : mesg.options
changes : if mesg.changes then mesg_id
cb : (err, result) =>
if result?.action == 'close'
err = 'close'
if err
dbg("user_query(query='#{misc.to_json(query)}') error:", err)
if @_query_changefeeds?[mesg_id]
delete @_query_changefeeds[mesg_id]
@error_to_client(id:mesg_id, error:err)
if mesg.changes and not first and @_query_changefeeds?[mesg_id]?
dbg("changefeed got messed up, so cancel it:")
@database.user_query_cancel_changefeed(id : mesg_id)
else
if mesg.changes and not first
resp = result
resp.id = mesg_id
resp.multi_response = true
else
first = false
resp = mesg
resp.query = result
@push_to_client(resp)
query_cancel_all_changefeeds: (cb) =>
if not @_query_changefeeds?
cb?(); return
cnt = misc.len(@_query_changefeeds)
if cnt == 0
cb?(); return
dbg = @dbg("query_cancel_all_changefeeds")
v = @_query_changefeeds
dbg("cancel #{cnt} changefeeds")
delete @_query_changefeeds
f = (id, cb) =>
dbg("cancel id=#{id}")
@database.user_query_cancel_changefeed
id : id
cb : (err) =>
if err
dbg("FEED: warning #{id} -- error canceling a changefeed #{misc.to_json(err)}")
else
dbg("FEED: canceled changefeed -- #{id}")
cb()
async.map(misc.keys(v), f, (err) => cb?(err))
mesg_query_cancel: (mesg) =>
if not @_query_changefeeds?[mesg.id]?
@success_to_client(id:mesg.id)
else
if @_query_changefeeds?
delete @_query_changefeeds[mesg.id]
@database.user_query_cancel_changefeed
id : mesg.id
cb : (err, resp) =>
if err
@error_to_client(id:mesg.id, error:err)
else
mesg.resp = resp
@push_to_client(mesg)
mesg_query_get_changefeed_ids: (mesg) =>
mesg.changefeed_ids = @_query_changefeeds ? {}
@push_to_client(mesg)
mesg_get_usernames: (mesg) =>
if not @account_id?
@error_to_client(id:mesg.id, error:"user must be signed in")
return
@database.get_usernames
account_ids : mesg.account_ids
use_cache : true
cb : (err, usernames) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.usernames(usernames:usernames, id:mesg.id))
mesg_create_support_ticket: (mesg) =>
dbg = @dbg("mesg_create_support_ticket")
dbg("#{misc.to_json(mesg)}")
m = underscore.omit(mesg, 'id', 'event')
get_support().create_ticket m, (err, url) =>
dbg("callback being called with #{err} and url: #{url}")
if err?
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(
message.support_ticket_url(id:mesg.id, url: url))
mesg_get_support_tickets: (mesg) =>
dbg = @dbg("mesg_get_support_tickets")
dbg("#{misc.to_json(mesg)}")
if not @account_id
err = "You must be signed in to use support related functions."
@error_to_client(id:mesg.id, error:err)
return
get_support().get_support_tickets @account_id, (err, tickets) =>
if err?
@error_to_client(id:mesg.id, error:err)
else
dbg("tickets: #{misc.to_json(tickets)}")
@push_to_client(
message.support_tickets(id:mesg.id, tickets: tickets))
ensure_fields: (mesg, fields) =>
if not mesg.id?
return false
if typeof(fields) == 'string'
fields = fields.split(' ')
for f in fields
if not mesg[f.trim()]?
err = "invalid message; must have #{f} field"
@error_to_client(id:mesg.id, error:err)
return false
return true
stripe_get_customer_id: (id, cb) =>
dbg = @dbg("stripe_get_customer_id")
dbg()
if not @account_id?
err = "You must be signed in to use billing related functions."
@error_to_client(id:id, error:err)
cb(err)
return
@_stripe = get_stripe()
if not @_stripe?
err = "stripe billing not configured"
dbg(err)
@error_to_client(id:id, error:err)
cb(err)
else
if @stripe_customer_id?
dbg("using cached @stripe_customer_id")
cb(undefined, @stripe_customer_id)
else
if @_stripe_customer_id_cbs?
@_stripe_customer_id_cbs.push({id:id, cb:cb})
return
@_stripe_customer_id_cbs = [{id:id, cb:cb}]
dbg('getting stripe_customer_id from db...')
@database.get_stripe_customer_id
account_id : @account_id
cb : (err, customer_id) =>
@stripe_customer_id = customer_id
for x in @_stripe_customer_id_cbs
{id, cb} = x
if err
dbg("fail -- #{err}")
@error_to_client(id:id, error:err)
cb(err)
else
dbg("got result #{customer_id}")
cb(undefined, customer_id)
delete @_stripe_customer_id_cbs
stripe_need_customer_id: (id, cb) =>
@dbg("stripe_need_customer_id")()
@stripe_get_customer_id id, (err, customer_id) =>
if err
cb(err); return
if not customer_id?
err = "customer not defined"
@stripe_error_to_client(id:id, error:err)
cb(err); return
cb(undefined, customer_id)
stripe_get_customer: (id, cb) =>
dbg = @dbg("stripe_get_customer")
dbg("getting id")
@stripe_get_customer_id id, (err, customer_id) =>
if err
dbg("failed -- #{err}")
cb(err)
return
if not customer_id?
dbg("no customer_id set yet")
cb(undefined, undefined)
return
dbg("now getting stripe customer object")
@_stripe.customers.retrieve customer_id, (err, customer) =>
if err
dbg("failed -- #{err}")
@error_to_client(id:id, error:err)
cb(err)
else
dbg("got it")
cb(undefined, customer)
stripe_error_to_client: (opts) =>
opts = defaults opts,
id : required
error : required
err = opts.error
if typeof(err) != 'string'
if err.stack?
err = err.stack.split('\n')[0]
else
err = misc.to_json(err)
@dbg("stripe_error_to_client")(err)
@error_to_client(id:opts.id, error:err)
mesg_stripe_get_customer: (mesg) =>
dbg = @dbg("mesg_stripe_get_customer")
dbg("get information from stripe about this customer, e.g., subscriptions, payment methods, etc.")
@stripe_get_customer mesg.id, (err, customer) =>
if err
return
resp = message.stripe_customer
id : mesg.id
stripe_publishable_key : @_stripe?.publishable_key
customer : customer
@push_to_client(resp)
mesg_stripe_create_source: (mesg) =>
dbg = @dbg("mesg_stripe_get_customer")
dbg("create a payment method (credit card) in stripe for this user")
if not @ensure_fields(mesg, 'token')
dbg("missing token field -- bailing")
return
dbg("looking up customer")
@stripe_get_customer_id mesg.id, (err, customer_id) =>
if err
dbg("failed -- #{err}")
return
if not customer_id?
dbg("create new stripe customer (from card token)")
description = undefined
email = undefined
async.series([
(cb) =>
dbg("get identifying info about user")
@database.get_account
columns : ['email_address', 'first_name', 'last_name']
account_id : @account_id
cb : (err, r) =>
if err
cb(err)
else
email = r.email_address
description = "#{r.first_name} #{r.last_name}"
dbg("they are #{description} with email #{email}")
cb()
(cb) =>
dbg("creating stripe customer")
x =
source : mesg.token
description : description
email : email
metadata :
account_id : @account_id
@_stripe.customers.create x, (err, customer) =>
if err
cb(err)
else
customer_id = customer.id
cb()
(cb) =>
dbg("success; now save customer id token to database")
@database.set_stripe_customer_id
account_id : @account_id
customer_id : customer_id
cb : cb
(cb) =>
dbg("success; sync user account with stripe")
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
dbg("failed -- #{err}")
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
else
dbg("add card to existing stripe customer")
async.series([
(cb) =>
@_stripe.customers.createCard(customer_id, {card:mesg.token}, cb)
(cb) =>
dbg("success; sync user account with stripe")
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_delete_source: (mesg) =>
dbg = @dbg("mesg_stripe_delete_source")
dbg("delete a payment method for this user")
if not @ensure_fields(mesg, 'card_id')
dbg("missing card_id field")
return
customer_id = undefined
async.series([
(cb) =>
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
(cb) =>
if not customer_id?
cb("no customer information so can't delete source")
else
@_stripe.customers.deleteCard(customer_id, mesg.card_id, cb)
(cb) =>
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_set_default_source: (mesg) =>
dbg = @dbg("mesg_stripe_set_default_source")
dbg("set a payment method for this user to be the default")
if not @ensure_fields(mesg, 'card_id')
dbg("missing field card_id")
return
customer_id = undefined
async.series([
(cb) =>
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
(cb) =>
if not customer_id?
cb("no customer information so can't update source")
else
dbg("now setting the default source in stripe")
@_stripe.customers.update(customer_id, {default_source:mesg.card_id}, cb)
(cb) =>
dbg("update database")
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
dbg("failed -- #{err}")
@stripe_error_to_client(id:mesg.id, error:err)
else
dbg("success")
@success_to_client(id:mesg.id)
)
mesg_stripe_update_source: (mesg) =>
dbg = @dbg("mesg_stripe_update_source")
dbg("modify a payment method")
if not @ensure_fields(mesg, 'card_id info')
return
if mesg.info.metadata?
@error_to_client(id:mesg.id, error:"you may not change card metadata")
return
customer_id = undefined
async.series([
(cb) =>
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
(cb) =>
if not customer_id?
cb("no customer information so can't update source")
else
@_stripe.customers.updateCard(customer_id, mesg.card_id, mesg.info, cb)
(cb) =>
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_get_plans: (mesg) =>
dbg = @dbg("mesg_stripe_get_plans")
dbg("get descriptions of the available plans that the user might subscribe to")
async.series([
(cb) =>
@stripe_get_customer_id(mesg.id, cb)
(cb) =>
@_stripe.plans.list (err, plans) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.stripe_plans(id: mesg.id, plans: plans))
])
mesg_stripe_create_subscription: (mesg) =>
dbg = @dbg("mesg_stripe_create_subscription")
dbg("create a subscription for this user, using some billing method")
if not @ensure_fields(mesg, 'plan')
@stripe_error_to_client(id:mesg.id, error:"missing field 'plan'")
return
schema = require('smc-util/schema').PROJECT_UPGRADES.membership[mesg.plan.split('-')[0]]
if not schema?
@stripe_error_to_client(id:mesg.id, error:"unknown plan -- '#{mesg.plan}'")
return
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
dbg("fail -- #{err}")
return
projects = mesg.projects
if not mesg.quantity?
mesg.quantity = 1
options =
plan : mesg.plan
quantity : mesg.quantity
coupon : mesg.coupon
subscription = undefined
tax_rate = undefined
async.series([
(cb) =>
dbg('determine applicable tax')
require('./stripe/sales-tax').stripe_sales_tax
customer_id : customer_id
cb : (err, rate) =>
tax_rate = rate
dbg("tax_rate = #{tax_rate}")
if tax_rate
options.tax_percent = Math.round(tax_rate*100*100)/100
cb(err)
(cb) =>
dbg("add customer subscription to stripe")
@_stripe.customers.createSubscription customer_id, options, (err, s) =>
if err
cb(err)
else
subscription = s
cb()
(cb) =>
if schema.cancel_at_period_end
dbg("Setting subscription to cancel at period end")
@_stripe.customers.cancelSubscription(customer_id, subscription.id, {at_period_end:true}, cb)
else
cb()
(cb) =>
dbg("Successfully added subscription; now save info in our database about subscriptions....")
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
dbg("fail -- #{err}")
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_cancel_subscription: (mesg) =>
dbg = @dbg("mesg_stripe_cancel_subscription")
dbg("cancel a subscription for this user")
if not @ensure_fields(mesg, 'subscription_id')
dbg("missing field subscription_id")
return
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
return
projects = undefined
subscription_id = mesg.subscription_id
async.series([
(cb) =>
dbg("cancel the subscription at stripe")
@_stripe.customers.cancelSubscription(customer_id, subscription_id, {at_period_end:mesg.at_period_end}, cb)
(cb) =>
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_update_subscription: (mesg) =>
dbg = @dbg("mesg_stripe_update_subscription")
dbg("edit a subscription for this user")
if not @ensure_fields(mesg, 'subscription_id')
dbg("missing field subscription_id")
return
subscription_id = mesg.subscription_id
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
return
subscription = undefined
async.series([
(cb) =>
dbg("Update the subscription.")
changes =
quantity : mesg.quantity
plan : mesg.plan
coupon : mesg.coupon
@_stripe.customers.updateSubscription(customer_id, subscription_id, changes, cb)
(cb) =>
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
], (err) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_stripe_get_subscriptions: (mesg) =>
dbg = @dbg("mesg_stripe_get_subscriptions")
dbg("get a list of all the subscriptions that this customer has")
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
return
options =
limit : mesg.limit
ending_before : mesg.ending_before
starting_after : mesg.starting_after
@_stripe.customers.listSubscriptions customer_id, options, (err, subscriptions) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.stripe_subscriptions(id:mesg.id, subscriptions:subscriptions))
mesg_stripe_get_charges: (mesg) =>
dbg = @dbg("mesg_stripe_get_charges")
dbg("get a list of charges for this customer.")
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
return
options =
customer : customer_id
limit : mesg.limit
ending_before : mesg.ending_before
starting_after : mesg.starting_after
@_stripe.charges.list options, (err, charges) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.stripe_charges(id:mesg.id, charges:charges))
mesg_stripe_get_invoices: (mesg) =>
dbg = @dbg("mesg_stripe_get_invoices")
dbg("get a list of invoices for this customer.")
@stripe_need_customer_id mesg.id, (err, customer_id) =>
if err
return
options =
customer : customer_id
limit : mesg.limit
ending_before : mesg.ending_before
starting_after : mesg.starting_after
@_stripe.invoices.list options, (err, invoices) =>
if err
@stripe_error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.stripe_invoices(id:mesg.id, invoices:invoices))
mesg_stripe_admin_create_invoice_item: (mesg) =>
if not @user_is_in_group('admin')
@error_to_client(id:mesg.id, error:"must be logged in and a member of the admin group to create invoice items")
return
dbg = @dbg("mesg_stripe_admin_create_invoice_item")
@_stripe = get_stripe()
if not @_stripe?
err = "stripe billing not configured"
dbg(err)
@error_to_client(id:id, error:err)
return
customer_id = undefined
description = undefined
email = undefined
new_customer = true
async.series([
(cb) =>
dbg("check for existing stripe customer_id")
@database.get_account
columns : ['stripe_customer_id', 'email_address', 'first_name', 'last_name', 'account_id']
account_id : mesg.account_id
email_address : mesg.email_address
cb : (err, r) =>
if err
cb(err)
else
customer_id = r.stripe_customer_id
email = r.email_address
description = "#{r.first_name} #{r.last_name}"
mesg.account_id = r.account_id
cb()
(cb) =>
if customer_id?
new_customer = false
dbg("already signed up for stripe -- sync local user account with stripe")
@database.stripe_update_customer
account_id : mesg.account_id
stripe : get_stripe()
customer_id : customer_id
cb : cb
else
dbg("create stripe entry for this customer")
x =
description : description
email : email
metadata :
account_id : mesg.account_id
@_stripe.customers.create x, (err, customer) =>
if err
cb(err)
else
customer_id = customer.id
cb()
(cb) =>
if not new_customer
cb()
else
dbg("store customer id in our database")
@database.set_stripe_customer_id
account_id : mesg.account_id
customer_id : customer_id
cb : cb
(cb) =>
if not (mesg.amount? and mesg.description?)
dbg("no amount or description -- not creating an invoice")
cb()
else
dbg("now create the invoice item")
@_stripe.invoiceItems.create
customer : customer_id
amount : mesg.amount*100
currency : "usd"
description : mesg.description
,
(err, invoice_item) =>
if err
cb(err)
else
cb()
], (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@success_to_client(id:mesg.id)
)
mesg_api_key: (mesg) =>
api_key_action
database : @database
account_id : @account_id
password : mesg.password
action : mesg.action
cb : (err, api_key) =>
if err
@error_to_client(id:mesg.id, error:err)
else
if api_key?
@push_to_client(message.api_key_info(id:mesg.id, api_key:api_key))
else
@success_to_client(id:mesg.id)
mesg_user_auth: (mesg) =>
auth_token.get_user_auth_token
database : @database
account_id : @account_id
user_account_id : mesg.account_id
password : mesg.password
cb : (err, auth_token) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.user_auth_token(id:mesg.id, auth_token:auth_token))
mesg_revoke_auth_token: (mesg) =>
auth_token.revoke_user_auth_token
database : @database
auth_token : mesg.auth_token
cb : (err) =>
if err
@error_to_client(id:mesg.id, error:err)
else
@push_to_client(message.success(id:mesg.id))