Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39538
1
###
2
Client = a client that is connected via a persistent connection to the hub
3
###
4
5
{EventEmitter} = require('events')
6
7
uuid = require('node-uuid')
8
async = require('async')
9
Cookies = require('cookies') # https://github.com/jed/cookies
10
misc = require('smc-util/misc')
11
{defaults, required, to_safe_str} = misc
12
{JSON_CHANNEL} = require('smc-util/client')
13
message = require('smc-util/message')
14
base_url_lib = require('./base-url')
15
access = require('./access')
16
clients = require('./clients').get_clients()
17
auth = require('./auth')
18
auth_token = require('./auth-token')
19
password = require('./password')
20
local_hub_connection = require('./local_hub_connection')
21
sign_in = require('./sign-in')
22
smc_version = require('./hub-version')
23
hub_projects = require('./projects')
24
{get_stripe} = require('./stripe/connect')
25
{get_support} = require('./support')
26
{send_email} = require('./email')
27
{api_key_action} = require('./api/manage')
28
{create_account, delete_account} = require('./create-account')
29
30
underscore = require('underscore')
31
32
DEBUG2 = !!process.env.SMC_DEBUG2
33
34
REQUIRE_ACCOUNT_TO_EXECUTE_CODE = false
35
36
# Anti DOS parameters:
37
# If a client sends a burst of messages, we space handling them out by this many milliseconds:
38
# (this even includes keystrokes when using the terminal)
39
MESG_QUEUE_INTERVAL_MS = 0
40
# If a client sends a massive burst of messages, we discard all but the most recent this many of them:
41
# The client *should* be implemented in a way so that this never happens, and when that is
42
# the case -- according to our loging -- we might switch to immediately banning clients that
43
# hit these limits...
44
MESG_QUEUE_MAX_COUNT = 300
45
MESG_QUEUE_MAX_WARN = 50
46
47
# Any messages larger than this is dropped (it could take a long time to handle, by a de-JSON'ing attack, etc.).
48
MESG_QUEUE_MAX_SIZE_MB = 10
49
50
# How long to cache a positive authentication for using a project.
51
CACHE_PROJECT_AUTH_MS = 1000*60*15 # 15 minutes
52
53
# How long all info about a websocket Client connection
54
# is kept in memory after a user disconnects. This makes it
55
# so that if they quickly reconnect, the connections to projects
56
# and other state doesn't have to be recomputed.
57
CLIENT_DESTROY_TIMER_S = 60*10 # 10 minutes
58
#CLIENT_DESTROY_TIMER_S = 0.1 # instant -- for debugging
59
60
CLIENT_MIN_ACTIVE_S = 45 # ??? is this a good choice? No idea.
61
62
63
# recording metrics and statistics
64
MetricsRecorder = require('./metrics-recorder')
65
66
# setting up client metrics
67
mesg_from_client_total = MetricsRecorder.new_counter('mesg_from_client_total',
68
'counts Client::handle_json_message_from_client invocations', ['type', 'event'])
69
push_to_client_stats_h = MetricsRecorder.new_histogram('push_to_client_histo_ms', 'Client: push_to_client',
70
buckets : [1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000]
71
labels: ['event']
72
)
73
push_to_client_stats_q = MetricsRecorder.new_quantile('push_to_client_quant_ms', 'Client: push_to_client',
74
percentiles : [0, 0.25, 0.5, 0.75, 0.9, 0.99, 1]
75
labels: ['event']
76
)
77
push_to_client_to_json_summary = MetricsRecorder.new_summary('push_to_client_to_json',
78
'summary stats for Client::push_to_client/to_json', labels: ['event'])
79
80
uncaught_exception_total = MetricsRecorder.new_counter('uncaught_exception_total', 'counts "BUG"s')
81
82
class exports.Client extends EventEmitter
83
constructor: (opts) ->
84
@_opts = defaults opts,
85
conn : undefined
86
logger : undefined
87
database : required
88
compute_server : required
89
host : undefined
90
port : undefined
91
92
@conn = @_opts.conn
93
@logger = @_opts.logger
94
@database = @_opts.database
95
@compute_server = @_opts.compute_server
96
97
@_when_connected = new Date()
98
99
@_messages =
100
being_handled : {}
101
total_time : 0
102
count : 0
103
104
# The variable account_id is either undefined or set to the
105
# account id of the user that this session has successfully
106
# authenticated as. Use @account_id to decide whether or not
107
# it is safe to carry out a given action.
108
@account_id = undefined
109
110
if @conn?
111
# has a persistent connection, e.g., NOT just used for an API
112
@init_conn()
113
else
114
@id = misc.uuid()
115
116
init_conn: =>
117
# initialize everything related to persistent connections
118
@_data_handlers = {}
119
@_data_handlers[JSON_CHANNEL] = @handle_json_message_from_client
120
121
# The persistent sessions that this client starts.
122
@compute_session_uuids = []
123
124
@install_conn_handlers()
125
126
@ip_address = @conn.address.ip
127
128
# A unique id -- can come in handy
129
@id = @conn.id
130
131
# Setup remember-me related cookie handling
132
@cookies = {}
133
c = new Cookies(@conn.request)
134
@_remember_me_value = c.get(base_url_lib.base_url() + 'remember_me')
135
136
@check_for_remember_me()
137
138
# Security measure: check every 5 minutes that remember_me
139
# cookie used for login is still valid. If the cookie is gone
140
# and this fails, user gets a message, and see that they must sign in.
141
@_remember_me_interval = setInterval(@check_for_remember_me, 1000*60*5)
142
143
touch: (opts={}) =>
144
if not @account_id # not logged in
145
opts.cb?('not logged in')
146
return
147
opts = defaults opts,
148
project_id : undefined
149
path : undefined
150
action : 'edit'
151
force : false
152
cb : undefined
153
# touch -- indicate by changing field in database that this user is active.
154
# We do this at most once every CLIENT_MIN_ACTIVE_S seconds, for given choice
155
# of project_id, path (unless force is true).
156
if not @_touch_lock?
157
@_touch_lock = {}
158
key = "#{opts.project_id}-#{opts.path}-#{opts.action}"
159
if not opts.force and @_touch_lock[key]
160
opts.cb?("touch lock")
161
return
162
opts.account_id = @account_id
163
@_touch_lock[key] = true
164
delete opts.force
165
@database.touch(opts)
166
setTimeout((()=>delete @_touch_lock[key]), CLIENT_MIN_ACTIVE_S*1000)
167
168
install_conn_handlers: () =>
169
dbg = @dbg('install_conn_handlers')
170
if @_destroy_timer?
171
clearTimeout(@_destroy_timer)
172
delete @_destroy_timer
173
174
@conn.on "data", (data) =>
175
@handle_data_from_client(data)
176
177
@conn.on "end", () =>
178
dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address}) -- CLOSED")
179
# CRITICAL -- of course we need to cancel all changefeeds when user disconnects,
180
# even temporarily, since messages could be dropped otherwise. (The alternative is to
181
# cache all messages in the hub, which has serious memory implications.)
182
@query_cancel_all_changefeeds()
183
# Actually destroy Client in a few minutes, unless user reconnects
184
# to this session. Often the user may have a temporary network drop,
185
# and we keep everything waiting for them for short time
186
# in case this happens.
187
@_destroy_timer = setTimeout(@destroy, 1000*CLIENT_DESTROY_TIMER_S)
188
189
dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address}) ESTABLISHED")
190
191
dbg: (desc) =>
192
if @logger?.debug
193
return (m...) => @logger.debug("Client(#{@id}).#{desc}: #{JSON.stringify(m...)}")
194
else
195
return ->
196
197
destroy: () =>
198
dbg = @dbg('destroy')
199
dbg("destroy connection: hub <--> client(id=#{@id}, address=#{@ip_address}) -- CLOSED")
200
clearInterval(@_remember_me_interval)
201
@query_cancel_all_changefeeds()
202
@closed = true
203
@emit('close')
204
@compute_session_uuids = []
205
c = clients[@id]
206
delete clients[@id]
207
if c? and c.call_callbacks?
208
for id,f of c.call_callbacks
209
f("connection closed")
210
delete c.call_callbacks
211
for h in local_hub_connection.all_local_hubs()
212
h.free_resources_for_client_id(@id)
213
214
remember_me_failed: (reason) =>
215
return if not @conn?
216
@signed_out() # so can't do anything with projects, etc.
217
@push_to_client(message.remember_me_failed(reason:reason))
218
219
check_for_remember_me: () =>
220
return if not @conn?
221
dbg = @dbg("check_for_remember_me")
222
value = @_remember_me_value
223
if not value?
224
@remember_me_failed("no remember_me cookie")
225
return
226
x = value.split('$')
227
if x.length != 4
228
@remember_me_failed("invalid remember_me cookie")
229
return
230
hash = auth.generate_hash(x[0], x[1], x[2], x[3])
231
dbg("checking for remember_me cookie with hash='#{hash.slice(0,15)}...'") # don't put all in log -- could be dangerous
232
@database.get_remember_me
233
hash : hash
234
cb : (error, signed_in_mesg) =>
235
dbg("remember_me: got error", error, "signed_in_mesg", signed_in_mesg)
236
if error
237
@remember_me_failed("error accessing database")
238
return
239
if not signed_in_mesg?
240
@remember_me_failed("remember_me deleted or expired")
241
return
242
# sign them in if not already signed in
243
if @account_id != signed_in_mesg.account_id
244
signed_in_mesg.hub = @_opts.host + ':' + @_opts.port
245
@hash_session_id = hash
246
@signed_in(signed_in_mesg)
247
@push_to_client(signed_in_mesg)
248
249
cap_session_limits: (limits) ->
250
###
251
Capping resource limits; client can request anything.
252
We cap what they get based on the account type, etc...
253
This functions *modifies* the limits object in place.
254
###
255
if @account_id? # logged in
256
misc.min_object(limits, SESSION_LIMITS) # TODO
257
else
258
misc.min_object(limits, SESSION_LIMITS_NOT_LOGGED_IN) # TODO
259
260
push_to_client: (mesg, cb) =>
261
###
262
Pushing messages to this particular connected client
263
###
264
if @closed
265
cb?("disconnected")
266
return
267
dbg = @dbg("push_to_client")
268
269
if mesg.event != 'pong'
270
dbg("hub --> client (client=#{@id}): #{misc.trunc(to_safe_str(mesg),300)}")
271
272
if mesg.id?
273
start = @_messages.being_handled[mesg.id]
274
if start?
275
time_taken = new Date() - start
276
delete @_messages.being_handled[mesg.id]
277
@_messages.total_time += time_taken
278
@_messages.count += 1
279
avg = Math.round(@_messages.total_time / @_messages.count)
280
dbg("[#{time_taken} mesg_time_ms] [#{avg} mesg_avg_ms] -- mesg.id=#{mesg.id}")
281
push_to_client_stats_q.observe({event:mesg.event}, time_taken)
282
push_to_client_stats_h.observe({event:mesg.event}, time_taken)
283
284
# If cb *is* given and mesg.id is *not* defined, then
285
# we also setup a listener for a response from the client.
286
listen = cb? and not mesg.id?
287
if listen
288
# This message is not a response to a client request.
289
# Instead, we are initiating a request to the user and we
290
# want a result back (hence cb? being defined).
291
mesg.id = misc.uuid()
292
if not @call_callbacks?
293
@call_callbacks = {}
294
@call_callbacks[mesg.id] = cb
295
f = () =>
296
g = @call_callbacks?[mesg.id]
297
if g?
298
delete @call_callbacks[mesg.id]
299
g("timed out")
300
setTimeout(f, 15000) # timeout after some seconds
301
302
t = new Date()
303
json = misc.to_json_socket(mesg)
304
tm = new Date() - t
305
if tm > 10
306
dbg("mesg.id=#{mesg.id}: time to json=#{tm}ms; length=#{json.length}; value='#{misc.trunc(json, 500)}'")
307
@push_data_to_client(JSON_CHANNEL, json)
308
if not listen
309
cb?()
310
return
311
312
push_data_to_client: (channel, data) ->
313
return if not @conn?
314
if @closed
315
return
316
@conn.write(channel + data)
317
318
error_to_client: (opts) ->
319
opts = defaults opts,
320
id : undefined
321
error : required
322
@push_to_client(message.error(id:opts.id, error:opts.error))
323
324
success_to_client: (opts) ->
325
opts = defaults opts,
326
id : required
327
@push_to_client(message.success(id:opts.id))
328
329
signed_in: (signed_in_mesg) =>
330
return if not @conn?
331
# Call this method when the user has successfully signed in.
332
333
@signed_in_mesg = signed_in_mesg # save it, since the properties are handy to have.
334
335
# Record that this connection is authenticated as user with given uuid.
336
@account_id = signed_in_mesg.account_id
337
338
sign_in.record_sign_in
339
ip_address : @ip_address
340
successful : true
341
remember_me : signed_in_mesg.remember_me # True if sign in accomplished via rememember me token.
342
email_address : signed_in_mesg.email_address
343
account_id : signed_in_mesg.account_id
344
database : @database
345
346
# Get user's group from database.
347
@get_groups()
348
349
signed_out: () =>
350
@account_id = undefined
351
352
# Setting and getting HTTP-only cookies via Primus + AJAX
353
get_cookie: (opts) ->
354
opts = defaults opts,
355
name : required
356
cb : required # cb(value)
357
if not @conn?.id?
358
# no connection or connection died
359
return
360
@once("get_cookie-#{opts.name}", (value) -> opts.cb(value))
361
@push_to_client(message.cookies(id:@conn.id, get:opts.name, url:base_url_lib.base_url()+"/cookies"))
362
363
set_cookie: (opts) ->
364
opts = defaults opts,
365
name : required
366
value : required
367
ttl : undefined # time in seconds until cookie expires
368
if not @conn?.id?
369
# no connection or connection died
370
return
371
372
options = {}
373
if opts.ttl?
374
options.expires = new Date(new Date().getTime() + 1000*opts.ttl)
375
@cookies[opts.name] = {value:opts.value, options:options}
376
@push_to_client(message.cookies(id:@conn.id, set:opts.name, url:base_url_lib.base_url()+"/cookies", value:opts.value))
377
378
remember_me: (opts) ->
379
return if not @conn?
380
###
381
Remember me. There are many ways to implement
382
"remember me" functionality in a web app. Here's how
383
we do it with SMC: We generate a random uuid,
384
which along with salt, is stored in the user's
385
browser as an httponly cookie. We password hash the
386
random uuid and store that in our database. When
387
the user later visits the SMC site, their browser
388
sends the cookie, which the server hashes to get the
389
key for the database table, which has corresponding
390
value the mesg needed for sign in. We then sign the
391
user in using that message.
392
393
The reason we use a password hash is that if
394
somebody gains access to an entry in the key:value
395
store of the database, we want to ensure that they
396
can't use that information to login. The only way
397
they could login would be by gaining access to the
398
cookie in the user's browser.
399
400
There is no point in signing the cookie since its
401
contents are random.
402
403
Regarding ttl, we use 1 year. The database will forget
404
the cookie automatically at the same time that the
405
browser invalidates it.
406
###
407
408
# WARNING: The code below is somewhat replicated in
409
# passport_login.
410
411
opts = defaults opts,
412
email_address : required
413
account_id : required
414
ttl : 24*3600 *30 # 30 days, by default
415
cb : undefined
416
417
ttl = opts.ttl; delete opts.ttl
418
opts.hub = @_opts.host
419
opts.remember_me = true
420
421
opts0 = misc.copy(opts)
422
delete opts0.cb
423
signed_in_mesg = message.signed_in(opts0)
424
session_id = uuid.v4()
425
@hash_session_id = auth.password_hash(session_id)
426
427
x = @hash_session_id.split('$') # format: algorithm$salt$iterations$hash
428
@_remember_me_value = [x[0], x[1], x[2], session_id].join('$')
429
@set_cookie
430
name : base_url_lib.base_url() + 'remember_me'
431
value : @_remember_me_value
432
ttl : ttl
433
434
@database.save_remember_me
435
account_id : opts.account_id
436
hash : @hash_session_id
437
value : signed_in_mesg
438
ttl : ttl
439
cb : opts.cb
440
441
invalidate_remember_me: (opts) ->
442
return if not @conn?
443
444
opts = defaults opts,
445
cb : required
446
447
if @hash_session_id?
448
@database.delete_remember_me
449
hash : @hash_session_id
450
cb : opts.cb
451
else
452
opts.cb()
453
454
###
455
Our realtime socket connection might only support one connection
456
between the client and
457
server, so we multiplex multiple channels over the same
458
connection. There is one base channel for JSON messages called
459
JSON_CHANNEL, which themselves can be routed to different
460
callbacks, etc., by the client code. There are 16^4-1 other
461
channels, which are for sending raw data. The raw data messages
462
are prepended with a UTF-16 character that identifies the
463
channel. The channel character is random (which might be more
464
secure), and there is no relation between the channels for two
465
distinct clients.
466
###
467
468
handle_data_from_client: (data) =>
469
return if not @conn?
470
dbg = @dbg("handle_data_from_client")
471
## Only enable this when doing low level debugging -- performance impacts AND leakage of dangerous info!
472
if DEBUG2
473
dbg("handle_data_from_client('#{misc.trunc(data.toString(),400)}')")
474
475
# TODO: THIS IS A SIMPLE anti-DOS measure; it might be too
476
# extreme... we shall see. It prevents a number of attacks,
477
# e.g., users storing a multi-gigabyte worksheet title,
478
# etc..., which would (and will) otherwise require care with
479
# every single thing we store.
480
481
# TODO: the two size things below should be specific messages (not generic error_to_client), and
482
# be sensibly handled by the client.
483
if data.length >= MESG_QUEUE_MAX_SIZE_MB * 10000000
484
# We don't parse it, we don't look at it, we don't know it's id. This shouldn't ever happen -- and probably would only
485
# happen because of a malicious attacker. JSON parsing arbitrarily large strings would
486
# be very dangerous, and make crashing the server way too easy.
487
# We just respond with this error below. The client should display to the user all id-less errors.
488
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."
489
@logger?.error(msg)
490
@error_to_client(error:msg)
491
return
492
493
if data.length == 0
494
msg = "The server ignored a message since it was empty."
495
@logger?.error(msg)
496
@error_to_client(error:msg)
497
return
498
499
if not @_handle_data_queue?
500
@_handle_data_queue = []
501
502
channel = data[0]
503
h = @_data_handlers[channel]
504
505
if not h?
506
if channel != 'X' # X is a special case used on purpose -- not an error.
507
@logger?.error("unable to handle data on an unknown channel: '#{channel}', '#{data}'")
508
# Tell the client that they had better reconnect.
509
@push_to_client( message.session_reconnect(data_channel : channel) )
510
return
511
512
# The rest of the function is basically the same as "h(data.slice(1))", except that
513
# it ensure that if there is a burst of messages, then (1) we handle at most 1 message
514
# per client every MESG_QUEUE_INTERVAL_MS, and we drop messages if there are too many.
515
# This is an anti-DOS measure.
516
517
@_handle_data_queue.push([h, data.slice(1)])
518
519
if @_handle_data_queue_empty_function?
520
return
521
522
# define a function to empty the queue
523
@_handle_data_queue_empty_function = () =>
524
if @_handle_data_queue.length == 0
525
# done doing all tasks
526
delete @_handle_data_queue_empty_function
527
return
528
529
if @_handle_data_queue.length > MESG_QUEUE_MAX_WARN
530
dbg("MESG_QUEUE_MAX_WARN(=#{MESG_QUEUE_MAX_WARN}) exceeded (=#{@_handle_data_queue.length}) -- just a warning")
531
532
# drop oldest message to keep
533
if @_handle_data_queue.length > MESG_QUEUE_MAX_COUNT
534
dbg("MESG_QUEUE_MAX_COUNT(=#{MESG_QUEUE_MAX_COUNT}) exceeded (=#{@_handle_data_queue.length}) -- drop oldest messages")
535
while @_handle_data_queue.length > MESG_QUEUE_MAX_COUNT
536
discarded_mesg = @_handle_data_queue.shift()
537
data = discarded_mesg?[1]
538
dbg("discarded_mesg='#{misc.trunc(data?.toString?(),1000)}'")
539
540
541
# get task
542
task = @_handle_data_queue.shift()
543
# do task
544
task[0](task[1])
545
# do next one in >= MESG_QUEUE_INTERVAL_MS
546
setTimeout( @_handle_data_queue_empty_function, MESG_QUEUE_INTERVAL_MS )
547
548
@_handle_data_queue_empty_function()
549
550
register_data_handler: (h) ->
551
return if not @conn?
552
# generate a channel character that isn't already taken -- if these get too large,
553
# this will break (see, e.g., http://blog.fgribreau.com/2012/05/how-to-fix-could-not-decode-text-frame.html);
554
# however, this is a counter for *each* individual user connection, so they won't get too big.
555
# Ultimately, we'll redo things to use primus/websocket channel support, which should be much more powerful
556
# and faster.
557
if not @_last_channel?
558
@_last_channel = 1
559
while true
560
@_last_channel += 1
561
channel = String.fromCharCode(@_last_channel)
562
if not @_data_handlers[channel]?
563
break
564
@_data_handlers[channel] = h
565
return channel
566
567
###
568
Message handling functions:
569
570
Each function below that starts with mesg_ handles a given
571
message type (an event). The implementations of many of the
572
handlers are somewhat long/involved, so the function below
573
immediately calls another function defined elsewhere. This will
574
make it easier to refactor code to other modules, etc., later.
575
This approach also clarifies what exactly about this object
576
is used to implement the relevant functionality.
577
###
578
handle_json_message_from_client: (data) =>
579
return if not @conn?
580
if @_ignore_client
581
return
582
try
583
mesg = misc.from_json_socket(data)
584
catch error
585
@logger?.error("error parsing incoming mesg (invalid JSON): #{mesg}")
586
return
587
dbg = @dbg('handle_json_message_from_client')
588
if mesg.event != 'ping'
589
dbg("hub <-- client: #{misc.trunc(to_safe_str(mesg), 120)}")
590
591
# check for message that is coming back in response to a request from the hub
592
if @call_callbacks? and mesg.id?
593
f = @call_callbacks[mesg.id]
594
if f?
595
delete @call_callbacks[mesg.id]
596
f(undefined, mesg)
597
return
598
599
if mesg.id?
600
@_messages.being_handled[mesg.id] = new Date()
601
602
handler = @["mesg_#{mesg.event}"]
603
if handler?
604
handler(mesg)
605
else
606
@push_to_client(message.error(error:"Hub does not know how to handle a '#{mesg.event}' event.", id:mesg.id))
607
if mesg.event == 'get_all_activity'
608
dbg("ignoring all further messages from old client=#{@id}")
609
@_ignore_client = true
610
611
mesg_ping: (mesg) =>
612
@push_to_client(message.pong(id:mesg.id, now:new Date()))
613
614
# Messages: Sessions
615
mesg_start_session: (mesg) =>
616
if REQUIRE_ACCOUNT_TO_EXECUTE_CODE and not @account_id?
617
@push_to_client(message.error(id:mesg.id, error:"You must be signed in to start a session."))
618
return
619
620
switch mesg.type
621
when 'console'
622
@connect_to_console_session(mesg)
623
else
624
@error_to_client(id:mesg.id, error:"Unknown message type '#{mesg.type}'")
625
626
mesg_connect_to_session: (mesg) =>
627
if REQUIRE_ACCOUNT_TO_EXECUTE_CODE and not @account_id?
628
@push_to_client(message.error(id:mesg.id, error:"You must be signed in to start a session."))
629
return
630
switch mesg.type
631
when 'console'
632
if not mesg.params?.path? or not mesg.params?.filename?
633
@push_to_client(message.error(id:mesg.id, error:"console session path and filename must be defined"))
634
return
635
@connect_to_console_session(mesg)
636
else
637
# TODO
638
@push_to_client(message.error(id:mesg.id, error:"Connecting to session of type '#{mesg.type}' not yet implemented"))
639
640
connect_to_console_session: (mesg) =>
641
# TODO -- implement read-only console sessions too (easy and amazing).
642
@get_project mesg, 'write', (err, project) =>
643
if not err # get_project sends error to client
644
project.console_session
645
client : @
646
params : mesg.params
647
session_uuid : mesg.session_uuid
648
cb : (err, connect_mesg) =>
649
if err
650
@error_to_client(id:mesg.id, error:err)
651
else
652
connect_mesg.id = mesg.id
653
@push_to_client(connect_mesg)
654
655
mesg_terminate_session: (mesg) =>
656
@get_project mesg, 'write', (err, project) =>
657
if not err # get_project sends error to client
658
project.terminate_session
659
session_uuid : mesg.session_uuid
660
cb : (err, resp) =>
661
if err
662
@error_to_client(id:mesg.id, error:err)
663
else
664
@push_to_client(mesg) # same message back.
665
666
# Messages: Account creation, deletion, sign in, sign out
667
mesg_create_account: (mesg) =>
668
create_account
669
client : @
670
mesg : mesg
671
database : @database
672
logger : @logger
673
host : @_opts.host
674
port : @_opts.port
675
sign_in : @conn? # browser clients have a websocket conn
676
677
mesg_delete_account: (mesg) =>
678
delete_account
679
client : @
680
mesg : mesg
681
database : @database
682
logger : @logger
683
684
mesg_sign_in: (mesg) =>
685
sign_in.sign_in
686
client : @
687
mesg : mesg
688
logger : @logger
689
database : @database
690
host : @_opts.host
691
port : @_opts.port
692
693
mesg_sign_in_using_auth_token: (mesg) =>
694
sign_in.sign_in_using_auth_token
695
client : @
696
mesg : mesg
697
logger : @logger
698
database : @database
699
host : @_opts.host
700
port : @_opts.port
701
702
mesg_sign_out: (mesg) =>
703
if not @account_id?
704
@push_to_client(message.error(id:mesg.id, error:"Not signed in."))
705
return
706
707
if mesg.everywhere
708
# invalidate all remeber_me cookies
709
@database.invalidate_all_remember_me
710
account_id : @account_id
711
@signed_out() # deletes @account_id... so must be below database call above
712
# invalidate the remember_me on this browser
713
@invalidate_remember_me
714
cb:(error) =>
715
@dbg('mesg_sign_out')("signing out: #{mesg.id}, #{error}")
716
if not error
717
@push_to_client(message.error(id:mesg.id, error:error))
718
else
719
@push_to_client(message.signed_out(id:mesg.id))
720
721
# Messages: Password/email address management
722
mesg_change_password: (mesg) =>
723
password.change_password
724
mesg : mesg
725
account_id : @account_id
726
ip_address : @ip_address
727
database : @database
728
cb : (err) =>
729
@push_to_client(message.changed_password(id:mesg.id, error:err))
730
731
mesg_forgot_password: (mesg) =>
732
password.forgot_password
733
mesg : mesg
734
ip_address : @ip_address
735
database : @database
736
cb : (err) =>
737
@push_to_client(message.forgot_password_response(id:mesg.id, error:err))
738
739
mesg_reset_forgot_password: (mesg) =>
740
password.reset_forgot_password
741
mesg : mesg
742
database : @database
743
cb : (err) =>
744
@push_to_client(message.reset_forgot_password_response(id:mesg.id, error:err))
745
746
mesg_change_email_address: (mesg) =>
747
password.change_email_address
748
mesg : mesg
749
account_id : @account_id
750
ip_address : @ip_address
751
database : @database
752
logger : @logger
753
cb : (err) =>
754
@push_to_client(message.changed_email_address(id:mesg.id, error:err))
755
756
mesg_unlink_passport: (mesg) =>
757
if not @account_id?
758
@error_to_client(id:mesg.id, error:"must be logged in")
759
else
760
@database.delete_passport
761
account_id : @account_id
762
strategy : mesg.strategy
763
id : mesg.id
764
cb : (err) =>
765
if err
766
@error_to_client(id:mesg.id, error:err)
767
else
768
@success_to_client(id:mesg.id)
769
770
# Messages: Account settings
771
get_groups: (cb) =>
772
# see note above about our "infinite caching". Maybe a bad idea.
773
if @groups?
774
cb?(undefined, @groups)
775
return
776
@database.get_account
777
columns : ['groups']
778
account_id : @account_id
779
cb : (err, r) =>
780
if err
781
cb?(err)
782
else
783
@groups = r['groups']
784
cb?(undefined, @groups)
785
786
# Messages: Log errors that client sees so we can also look at them
787
mesg_log_client_error: (mesg) =>
788
@dbg('mesg_log_client_error')(mesg.error)
789
if not mesg.type?
790
mesg.type = "error"
791
if not mesg.error?
792
mesg.error = "error"
793
@database.log_client_error
794
event : mesg.type
795
error : mesg.error
796
account_id : @account_id
797
cb : (err) =>
798
if not mesg.id?
799
return
800
if err
801
@error_to_client(id:mesg.id, error:err)
802
else
803
@success_to_client(id:mesg.id)
804
805
mesg_webapp_error: (mesg) =>
806
@dbg('mesg_webapp_error')(mesg.msg)
807
mesg = misc.copy_without(mesg, 'event')
808
mesg.account_id = @account_id
809
@database.webapp_error(mesg)
810
811
# Messages: Project Management
812
get_project: (mesg, permission, cb) =>
813
###
814
How to use this: Either call the callback with the project, or if an error err
815
occured, call @error_to_client(id:mesg.id, error:err) and *NEVER*
816
call the callback. This function is meant to be used in a bunch
817
of the functions below for handling requests.
818
819
mesg -- must have project_id field
820
permission -- must be "read" or "write"
821
cb(err, project)
822
*NOTE*: on failure, if mesg.id is defined, then client will receive
823
an error message; the function calling get_project does *NOT*
824
have to send the error message back to the client!
825
###
826
dbg = @dbg('get_project')
827
828
err = undefined
829
if not mesg.project_id?
830
err = "mesg must have project_id attribute -- #{to_safe_str(mesg)}"
831
else if not @account_id?
832
err = "user must be signed in before accessing projects"
833
834
if err
835
if mesg.id?
836
@error_to_client(id:mesg.id, error:err)
837
cb(err)
838
return
839
840
key = mesg.project_id + permission
841
project = @_project_cache?[key]
842
if project?
843
# Use the cached project so we don't have to re-verify authentication
844
# for the user again below, which
845
# is very expensive. This cache does expire, in case user
846
# is kicked out of the project.
847
cb(undefined, project)
848
return
849
850
dbg()
851
async.series([
852
(cb) =>
853
switch permission
854
when 'read'
855
access.user_has_read_access_to_project
856
project_id : mesg.project_id
857
account_id : @account_id
858
account_groups : @groups
859
database : @database
860
cb : (err, result) =>
861
if err
862
cb("Internal error determining user permission -- #{err}")
863
else if not result
864
cb("User #{@account_id} does not have read access to project #{mesg.project_id}")
865
else
866
# good to go
867
cb()
868
when 'write'
869
access.user_has_write_access_to_project
870
database : @database
871
project_id : mesg.project_id
872
account_groups : @groups
873
account_id : @account_id
874
cb : (err, result) =>
875
if err
876
cb("Internal error determining user permission -- #{err}")
877
else if not result
878
cb("User #{@account_id} does not have write access to project #{mesg.project_id}")
879
else
880
# good to go
881
cb()
882
else
883
cb("Internal error -- unknown permission type '#{permission}'")
884
], (err) =>
885
if err
886
if mesg.id?
887
@error_to_client(id:mesg.id, error:err)
888
dbg("error -- #{err}")
889
cb(err)
890
else
891
project = hub_projects.new_project(mesg.project_id, @database, @compute_server)
892
@database.touch_project(project_id:mesg.project_id)
893
if not @_project_cache?
894
@_project_cache = {}
895
@_project_cache[key] = project
896
# cache for a while
897
setTimeout((()=>delete @_project_cache[key]), CACHE_PROJECT_AUTH_MS)
898
dbg("got project; caching and returning")
899
cb(undefined, project)
900
)
901
902
mesg_create_project: (mesg) =>
903
if not @account_id?
904
@error_to_client(id: mesg.id, error: "You must be signed in to create a new project.")
905
return
906
@touch()
907
908
dbg = @dbg('mesg_create_project')
909
910
project_id = undefined
911
project = undefined
912
location = undefined
913
914
async.series([
915
(cb) =>
916
dbg("create project entry in database")
917
@database.create_project
918
account_id : @account_id
919
title : mesg.title
920
description : mesg.description
921
cb : (err, _project_id) =>
922
project_id = _project_id; cb(err)
923
(cb) =>
924
dbg("open project...")
925
# We do the open/state below so that when user tries to open it in a moment it opens more quickly;
926
# also, in single dev mode, this ensures that project path is created, so can copy
927
# files to the project, etc.
928
# Also, if mesg.start is set, the project gets started below.
929
@compute_server.project
930
project_id : project_id
931
cb : (err, project) =>
932
if err
933
dbg("failed to get project -- #{err}")
934
else
935
async.series([
936
(cb) =>
937
project.open(cb:cb)
938
(cb) =>
939
project.state(cb:cb, force:true, update:true)
940
(cb) =>
941
if mesg.start
942
project.start(cb:cb)
943
else
944
dbg("not auto-starting the new project")
945
cb()
946
], (err) =>
947
dbg("open project and get state: #{err}")
948
)
949
cb() # we don't need to wait for project to open before responding to user that project was created.
950
], (err) =>
951
if err
952
dbg("error; project #{project_id} -- #{err}")
953
@error_to_client(id: mesg.id, error: "Failed to create new project '#{mesg.title}' -- #{misc.to_json(err)}")
954
else
955
dbg("SUCCESS: project #{project_id}")
956
@push_to_client(message.project_created(id:mesg.id, project_id:project_id))
957
# As an optimization, we start the process of opening the project, since the user is likely
958
# to open the project soon anyways.
959
dbg("start process of opening project")
960
@get_project {project_id:project_id}, 'write', (err, project) =>
961
)
962
963
mesg_write_text_file_to_project: (mesg) =>
964
@get_project mesg, 'write', (err, project) =>
965
if err
966
return
967
project.write_file
968
path : mesg.path
969
data : mesg.content
970
cb : (err) =>
971
if err
972
@error_to_client(id:mesg.id, error:err)
973
else
974
@push_to_client(message.file_written_to_project(id:mesg.id))
975
976
mesg_read_text_file_from_project: (mesg) =>
977
@get_project mesg, 'read', (err, project) =>
978
if err
979
return
980
project.read_file
981
path : mesg.path
982
cb : (err, content) =>
983
if err
984
@error_to_client(id:mesg.id, error:err)
985
else
986
t = content.blob.toString()
987
@push_to_client(message.text_file_read_from_project(id:mesg.id, content:t))
988
989
mesg_project_exec: (mesg) =>
990
if mesg.command == "ipython-notebook"
991
# we just drop these messages, which are from old non-updated clients (since we haven't
992
# written code yet to not allow them to connect -- TODO!).
993
return
994
@get_project mesg, 'write', (err, project) =>
995
if err
996
return
997
project.call
998
mesg : mesg
999
timeout : mesg.timeout
1000
cb : (err, resp) =>
1001
if err
1002
@error_to_client(id:mesg.id, error:err)
1003
else
1004
@push_to_client(resp)
1005
1006
mesg_copy_path_between_projects: (mesg) =>
1007
@touch()
1008
if not mesg.src_project_id?
1009
@error_to_client(id:mesg.id, error:"src_project_id must be defined")
1010
return
1011
if not mesg.target_project_id?
1012
@error_to_client(id:mesg.id, error:"target_project_id must be defined")
1013
return
1014
if not mesg.src_path?
1015
@error_to_client(id:mesg.id, error:"src_path must be defined")
1016
return
1017
1018
async.series([
1019
(cb) =>
1020
# Check permissions for the source and target projects (in parallel) --
1021
# need read access to the source and write access to the target.
1022
async.parallel([
1023
(cb) =>
1024
access.user_has_read_access_to_project
1025
project_id : mesg.src_project_id
1026
account_id : @account_id
1027
account_groups : @groups
1028
database : @database
1029
cb : (err, result) =>
1030
if err
1031
cb(err)
1032
else if not result
1033
cb("user must have read access to source project #{mesg.src_project_id}")
1034
else
1035
cb()
1036
(cb) =>
1037
access.user_has_write_access_to_project
1038
database : @database
1039
project_id : mesg.target_project_id
1040
account_id : @account_id
1041
account_groups : @groups
1042
cb : (err, result) =>
1043
if err
1044
cb(err)
1045
else if not result
1046
cb("user must have write access to target project #{mesg.target_project_id}")
1047
else
1048
cb()
1049
], cb)
1050
1051
(cb) =>
1052
# do the copy
1053
@compute_server.project
1054
project_id : mesg.src_project_id
1055
cb : (err, project) =>
1056
if err
1057
cb(err); return
1058
else
1059
project.copy_path
1060
path : mesg.src_path
1061
target_project_id : mesg.target_project_id
1062
target_path : mesg.target_path
1063
overwrite_newer : mesg.overwrite_newer
1064
delete_missing : mesg.delete_missing
1065
backup : mesg.backup
1066
timeout : mesg.timeout
1067
exclude_history : mesg.exclude_history
1068
cb : cb
1069
], (err) =>
1070
if err
1071
@error_to_client(id:mesg.id, error:err)
1072
else
1073
@push_to_client(message.success(id:mesg.id))
1074
)
1075
1076
mesg_local_hub: (mesg) =>
1077
###
1078
Directly communicate with the local hub. If the
1079
client has write access to the local hub, there's no
1080
reason they shouldn't be allowed to send arbitrary
1081
messages directly (they could anyways from the terminal).
1082
###
1083
dbg = @dbg('mesg_local_hub')
1084
dbg("hub --> local_hub: ", mesg)
1085
@get_project mesg, 'write', (err, project) =>
1086
if err
1087
return
1088
if not mesg.message?
1089
# in case the message itself is invalid -- is possible
1090
@error_to_client(id:mesg.id, error:"message must be defined")
1091
return
1092
1093
if mesg.message.event == 'project_exec' and mesg.message.command == "ipython-notebook"
1094
# we just drop these messages, which are from old non-updated clients (since we haven't
1095
# written code yet to not allow them to connect -- TODO!).
1096
return
1097
1098
# It's extremely useful if the local hub has a way to distinguish between different clients who are
1099
# being proxied through the same hub.
1100
mesg.message.client_id = @id
1101
1102
# Make the actual call
1103
project.call
1104
mesg : mesg.message
1105
timeout : mesg.timeout
1106
multi_response : mesg.multi_response
1107
cb : (err, resp) =>
1108
if err
1109
dbg("ERROR: #{err} calling message #{misc.to_json(mesg.message)}")
1110
@error_to_client(id:mesg.id, error:err)
1111
else
1112
if not mesg.multi_response
1113
resp.id = mesg.id
1114
@push_to_client(resp)
1115
1116
mesg_user_search: (mesg) =>
1117
if not mesg.limit? or mesg.limit > 50
1118
# hard cap at 50...
1119
mesg.limit = 50
1120
@touch()
1121
@database.user_search
1122
query : mesg.query
1123
limit : mesg.limit
1124
cb : (err, results) =>
1125
if err
1126
@error_to_client(id:mesg.id, error:err)
1127
else
1128
@push_to_client(message.user_search_results(id:mesg.id, results:results))
1129
1130
mesg_invite_collaborator: (mesg) =>
1131
@touch()
1132
@get_project mesg, 'write', (err, project) =>
1133
if err
1134
return
1135
# SECURITY NOTE: mesg.project_id is valid and the client has write access, since otherwise,
1136
# the @get_project function above wouldn't have returned without err...
1137
@database.add_user_to_project
1138
project_id : mesg.project_id
1139
account_id : mesg.account_id
1140
group : 'collaborator' # in future will be "invite_collaborator", once implemented
1141
cb : (err) =>
1142
if err
1143
@error_to_client(id:mesg.id, error:err)
1144
else
1145
@push_to_client(message.success(id:mesg.id))
1146
1147
mesg_invite_noncloud_collaborators: (mesg) =>
1148
dbg = @dbg('mesg_invite_noncloud_collaborators')
1149
@touch()
1150
@get_project mesg, 'write', (err, project) =>
1151
if err
1152
return
1153
1154
if mesg.to.length > 1024
1155
@error_to_client(id:mesg.id, error:"Specify less recipients when adding collaborators to project.")
1156
return
1157
1158
# users to invite
1159
to = (x for x in mesg.to.replace(/\s/g,",").replace(/;/g,",").split(',') when x)
1160
1161
# invitation template
1162
email = mesg.email
1163
1164
invite_user = (email_address, cb) =>
1165
dbg("inviting #{email_address}")
1166
if not misc.is_valid_email_address(email_address)
1167
cb("invalid email address '#{email_address}'")
1168
return
1169
email_address = misc.lower_email_address(email_address)
1170
if email_address.length >= 128
1171
# if an attacker tries to embed a spam in the email address itself (e.g, [email protected]), then
1172
# at least we can limit its size.
1173
cb("email address must be at most 128 characters: '#{email_address}'")
1174
return
1175
done = false
1176
account_id = undefined
1177
async.series([
1178
# already have an account?
1179
(cb) =>
1180
@database.account_exists
1181
email_address : email_address
1182
cb : (err, _account_id) =>
1183
dbg("account_exists: #{err}, #{_account_id}")
1184
account_id = _account_id
1185
cb(err)
1186
(cb) =>
1187
if account_id
1188
dbg("user #{email_address} already has an account -- add directly")
1189
# user has an account already
1190
done = true
1191
@database.add_user_to_project
1192
project_id : mesg.project_id
1193
account_id : account_id
1194
group : 'collaborator'
1195
cb : cb
1196
else
1197
dbg("user #{email_address} doesn't have an account yet -- may send email (if we haven't recently)")
1198
# create trigger so that when user eventually makes an account,
1199
# they will be added to the project.
1200
@database.account_creation_actions
1201
email_address : email_address
1202
action : {action:'add_to_project', group:'collaborator', project_id:mesg.project_id}
1203
ttl : 60*60*24*14 # valid for 14 days
1204
cb : cb
1205
(cb) =>
1206
if done
1207
cb()
1208
else
1209
@database.when_sent_project_invite
1210
project_id : mesg.project_id
1211
to : email_address
1212
cb : (err, when_sent) =>
1213
if err
1214
cb(err)
1215
else if when_sent - 0 >= misc.days_ago(7) - 0 # successfully sent < one week ago -- don't again
1216
done = true
1217
cb()
1218
else
1219
cb()
1220
(cb) =>
1221
if done
1222
cb()
1223
else
1224
cb()
1225
# send an email to the user -- async, not blocking user.
1226
# TODO: this can take a while -- we need to take some action
1227
# if it fails, e.g., change a setting in the projects table!
1228
subject = "CoCalc Invitation"
1229
# override subject if explicitly given
1230
if mesg.subject?
1231
subject = mesg.subject
1232
1233
if mesg.link2proj? # make sure invitees know where to go
1234
base_url = mesg.link2proj.split("/")
1235
base_url = "#{base_url[0]}//#{base_url[2]}"
1236
direct_link = "Then go to <a href='#{mesg.link2proj}'>the project '#{mesg.title}'</a>."
1237
else # fallback for outdated clients
1238
base_url = 'https://cocalc.com/'
1239
direct_link = ''
1240
1241
# asm_group: 699 is for invites https://app.sendgrid.com/suppressions/advanced_suppression_manager
1242
opts =
1243
to : email_address
1244
bcc : '[email protected]'
1245
fromname : 'CoCalc'
1246
from : '[email protected]'
1247
replyto : mesg.replyto ? '[email protected]'
1248
replyto_name : mesg.replyto_name
1249
subject : subject
1250
category : "invite"
1251
asm_group : 699
1252
body : email + """<br/><br/>
1253
<b>To accept the invitation, please sign up at
1254
<a href='#{base_url}'>#{base_url}</a>
1255
using exactly the email address '#{email_address}'.
1256
#{direct_link}</b><br/>"""
1257
cb : (err) =>
1258
if err
1259
dbg("FAILED to send email to #{email_address} -- err={misc.to_json(err)}")
1260
@database.sent_project_invite
1261
project_id : mesg.project_id
1262
to : email_address
1263
error : err
1264
send_email(opts)
1265
1266
], cb)
1267
1268
async.map to, invite_user, (err, results) =>
1269
if err
1270
@error_to_client(id:mesg.id, error:err)
1271
else
1272
@push_to_client(message.invite_noncloud_collaborators_resp(id:mesg.id, mesg:"Invited #{mesg.to} to collaborate on a project."))
1273
1274
mesg_remove_collaborator: (mesg) =>
1275
@touch()
1276
@get_project mesg, 'write', (err, project) =>
1277
if err
1278
return
1279
# See "Security note" in mesg_invite_collaborator
1280
@database.remove_collaborator_from_project
1281
project_id : mesg.project_id
1282
account_id : mesg.account_id
1283
cb : (err) =>
1284
if err
1285
@error_to_client(id:mesg.id, error:err)
1286
else
1287
@push_to_client(message.success(id:mesg.id))
1288
1289
1290
mesg_remove_blob_ttls: (mesg) =>
1291
if not @account_id?
1292
@push_to_client(message.error(id:mesg.id, error:"not yet signed in"))
1293
else
1294
@database.remove_blob_ttls
1295
uuids : mesg.uuids
1296
cb : (err) =>
1297
if err
1298
@error_to_client(id:mesg.id, error:err)
1299
else
1300
@push_to_client(message.success(id:mesg.id))
1301
1302
mesg_version: (mesg) =>
1303
# The version of the client...
1304
@smc_version = mesg.version
1305
@dbg('mesg_version')("client.smc_version=#{mesg.version}")
1306
if mesg.version < smc_version.version
1307
@push_version_update()
1308
1309
push_version_update: =>
1310
@push_to_client(message.version(version:smc_version.version, min_version:smc_version.min_browser_version))
1311
if smc_version.min_browser_version and @smc_version and @smc_version < smc_version.min_browser_version
1312
# Client is running an unsupported bad old version.
1313
# Brutally disconnect client! It's critical that they upgrade, since they are
1314
# causing problems or have major buggy code.
1315
if new Date() - @_when_connected <= 30000
1316
# If they just connected, kill the connection instantly
1317
@conn.end()
1318
else
1319
# Wait 1 minute to give them a chance to save data...
1320
setTimeout((()=>@conn.end()), 60000)
1321
1322
user_is_in_group: (group) =>
1323
return @groups? and group in @groups
1324
1325
mesg_project_set_quotas: (mesg) =>
1326
if not @user_is_in_group('admin')
1327
@error_to_client(id:mesg.id, error:"must be logged in and a member of the admin group to set project quotas")
1328
else if not misc.is_valid_uuid_string(mesg.project_id)
1329
@error_to_client(id:mesg.id, error:"invalid project_id")
1330
else
1331
project = undefined
1332
dbg = @dbg("mesg_project_set_quotas(project_id='#{mesg.project_id}')")
1333
async.series([
1334
(cb) =>
1335
dbg("update base quotas in the database")
1336
@database.set_project_settings
1337
project_id : mesg.project_id
1338
settings : misc.copy_without(mesg, ['event', 'id'])
1339
cb : cb
1340
(cb) =>
1341
dbg("get project from compute server")
1342
@compute_server.project
1343
project_id : mesg.project_id
1344
cb : (err, p) =>
1345
project = p; cb(err)
1346
(cb) =>
1347
dbg("determine total quotas and apply")
1348
project.set_all_quotas(cb:cb)
1349
], (err) =>
1350
if err
1351
@error_to_client(id:mesg.id, error:"problem setting project quota -- #{err}")
1352
else
1353
@push_to_client(message.success(id:mesg.id))
1354
)
1355
1356
###
1357
Public/published projects data
1358
###
1359
path_is_in_public_paths: (path, paths) =>
1360
return misc.path_is_in_public_paths(path, misc.keys(paths))
1361
1362
get_public_project: (opts) =>
1363
###
1364
Get a compute.Project object, or cb an error if the given
1365
path in the project isn't public. This is just like getting
1366
a project, but first ensures that given path is public.
1367
###
1368
opts = defaults opts,
1369
project_id : undefined
1370
path : undefined
1371
use_cache : true
1372
cb : required
1373
1374
if not opts.project_id?
1375
opts.cb("get_public_project: project_id must be defined")
1376
return
1377
1378
if not opts.path?
1379
opts.cb("get_public_project: path must be defined")
1380
return
1381
1382
# determine if path is public in given project, without using cache to determine paths; this *does* cache the result.
1383
@database.path_is_public
1384
project_id : opts.project_id
1385
path : opts.path
1386
cb : (err, is_public) =>
1387
if err
1388
opts.cb(err)
1389
return
1390
if is_public
1391
@compute_server.project
1392
project_id : opts.project_id
1393
cb : opts.cb
1394
else
1395
# no
1396
opts.cb("path '#{opts.path}' of project with id '#{opts.project_id}' is not public")
1397
1398
mesg_public_get_directory_listing: (mesg) =>
1399
dbg = @dbg('mesg_public_get_directory_listing')
1400
for k in ['path', 'project_id']
1401
if not mesg[k]?
1402
dbg("missing stuff in message")
1403
@error_to_client(id:mesg.id, error:"must specify #{k}")
1404
return
1405
1406
# We only require that there is at least one public path. If so,
1407
# we then get this listing and if necessary filter out the not public
1408
# entries in the listing.
1409
project = undefined
1410
listing = undefined
1411
async.series([
1412
(cb) =>
1413
dbg("checking for public path")
1414
@database.has_public_path
1415
project_id : mesg.project_id
1416
cb : (err, is_public) =>
1417
if err
1418
dbg("error checking -- #{err}")
1419
cb(err)
1420
else if not is_public
1421
dbg("no public paths at all -- deny all listings")
1422
cb("not_public") # be careful about changing this. This is a specific error we're giving now when a directory is not public.
1423
# Client figures out context and gives more detailed error message. Right now we use it in src/smc-webapp/project_files.cjsx
1424
# to provide user with helpful context based error about why they can't access a given directory
1425
else
1426
cb()
1427
(cb) =>
1428
dbg("get the project")
1429
@compute_server.project
1430
project_id : mesg.project_id
1431
cb : (err, x) =>
1432
project = x; cb(err)
1433
(cb) =>
1434
dbg("get the directory listing")
1435
project.directory_listing
1436
path : mesg.path
1437
hidden : mesg.hidden
1438
time : mesg.time
1439
start : mesg.start
1440
limit : mesg.limit
1441
cb : (err, x) =>
1442
listing = x; cb(err)
1443
(cb) =>
1444
dbg("filtering out public paths from listing")
1445
@database.filter_public_paths
1446
project_id : mesg.project_id
1447
path : mesg.path
1448
listing : listing
1449
cb : (err, x) =>
1450
listing = x; cb(err)
1451
], (err) =>
1452
if err
1453
dbg("something went wrong -- #{err}")
1454
@error_to_client(id:mesg.id, error:err)
1455
else
1456
dbg("it worked; telling client")
1457
@push_to_client(message.public_directory_listing(id:mesg.id, result:listing))
1458
)
1459
1460
mesg_public_get_text_file: (mesg) =>
1461
if not mesg.path?
1462
@error_to_client(id:mesg.id, error:'must specify path')
1463
return
1464
@get_public_project
1465
project_id : mesg.project_id
1466
path : mesg.path
1467
cb : (err, project) =>
1468
if err
1469
@error_to_client(id:mesg.id, error:err)
1470
return
1471
project.read_file
1472
path : mesg.path
1473
maxsize : 20000000 # restrict to 20MB limit
1474
cb : (err, data) =>
1475
if err
1476
@error_to_client(id:mesg.id, error:err)
1477
else
1478
# since this maybe be a Buffer... (depending on backend)
1479
if Buffer.isBuffer(data)
1480
data = data.toString('utf-8')
1481
@push_to_client(message.public_text_file_contents(id:mesg.id, data:data))
1482
1483
mesg_copy_public_path_between_projects: (mesg) =>
1484
@touch()
1485
if not mesg.src_project_id?
1486
@error_to_client(id:mesg.id, error:"src_project_id must be defined")
1487
return
1488
if not mesg.target_project_id?
1489
@error_to_client(id:mesg.id, error:"target_project_id must be defined")
1490
return
1491
if not mesg.src_path?
1492
@error_to_client(id:mesg.id, error:"src_path must be defined")
1493
return
1494
project = undefined
1495
async.series([
1496
(cb) =>
1497
# ensure user can write to the target project
1498
access.user_has_write_access_to_project
1499
database : @database
1500
project_id : mesg.target_project_id
1501
account_id : @account_id
1502
account_groups : @groups
1503
cb : (err, result) =>
1504
if err
1505
cb(err)
1506
else if not result
1507
cb("user must have write access to target project #{mesg.target_project_id}")
1508
else
1509
cb()
1510
(cb) =>
1511
@get_public_project
1512
project_id : mesg.src_project_id
1513
path : mesg.src_path
1514
cb : (err, x) =>
1515
project = x
1516
cb(err)
1517
(cb) =>
1518
project.copy_path
1519
path : mesg.src_path
1520
target_project_id : mesg.target_project_id
1521
target_path : mesg.target_path
1522
overwrite_newer : mesg.overwrite_newer
1523
delete_missing : mesg.delete_missing
1524
timeout : mesg.timeout
1525
exclude_history : mesg.exclude_history
1526
backup : mesg.backup
1527
cb : cb
1528
], (err) =>
1529
if err
1530
@error_to_client(id:mesg.id, error:err)
1531
else
1532
@push_to_client(message.success(id:mesg.id))
1533
)
1534
1535
###
1536
Data Query
1537
###
1538
mesg_query: (mesg) =>
1539
query = mesg.query
1540
if not query?
1541
@error_to_client(id:mesg.id, error:"malformed query")
1542
return
1543
dbg = @dbg("user_query")
1544
# CRITICAL: don't enable this except for serious debugging, since it can result in HUGE output
1545
#dbg("account_id=#{@account_id} makes query='#{misc.to_json(query)}'")
1546
first = true
1547
if mesg.changes
1548
@_query_changefeeds ?= {}
1549
@_query_changefeeds[mesg.id] = true
1550
mesg_id = mesg.id
1551
@database.user_query
1552
account_id : @account_id
1553
query : query
1554
options : mesg.options
1555
changes : if mesg.changes then mesg_id
1556
cb : (err, result) =>
1557
if result?.action == 'close'
1558
err = 'close'
1559
if err
1560
dbg("user_query(query='#{misc.to_json(query)}') error:", err)
1561
if @_query_changefeeds?[mesg_id]
1562
delete @_query_changefeeds[mesg_id]
1563
@error_to_client(id:mesg_id, error:err)
1564
if mesg.changes and not first and @_query_changefeeds?[mesg_id]?
1565
dbg("changefeed got messed up, so cancel it:")
1566
@database.user_query_cancel_changefeed(id : mesg_id)
1567
else
1568
if mesg.changes and not first
1569
resp = result
1570
resp.id = mesg_id
1571
resp.multi_response = true
1572
else
1573
first = false
1574
resp = mesg
1575
resp.query = result
1576
@push_to_client(resp)
1577
1578
query_cancel_all_changefeeds: (cb) =>
1579
if not @_query_changefeeds?
1580
cb?(); return
1581
cnt = misc.len(@_query_changefeeds)
1582
if cnt == 0
1583
cb?(); return
1584
dbg = @dbg("query_cancel_all_changefeeds")
1585
v = @_query_changefeeds
1586
dbg("cancel #{cnt} changefeeds")
1587
delete @_query_changefeeds
1588
f = (id, cb) =>
1589
dbg("cancel id=#{id}")
1590
@database.user_query_cancel_changefeed
1591
id : id
1592
cb : (err) =>
1593
if err
1594
dbg("FEED: warning #{id} -- error canceling a changefeed #{misc.to_json(err)}")
1595
else
1596
dbg("FEED: canceled changefeed -- #{id}")
1597
cb()
1598
async.map(misc.keys(v), f, (err) => cb?(err))
1599
1600
mesg_query_cancel: (mesg) =>
1601
if not @_query_changefeeds?[mesg.id]?
1602
# no such changefeed
1603
@success_to_client(id:mesg.id)
1604
else
1605
# actualy cancel it.
1606
if @_query_changefeeds?
1607
delete @_query_changefeeds[mesg.id]
1608
@database.user_query_cancel_changefeed
1609
id : mesg.id
1610
cb : (err, resp) =>
1611
if err
1612
@error_to_client(id:mesg.id, error:err)
1613
else
1614
mesg.resp = resp
1615
@push_to_client(mesg)
1616
1617
mesg_query_get_changefeed_ids: (mesg) =>
1618
mesg.changefeed_ids = @_query_changefeeds ? {}
1619
@push_to_client(mesg)
1620
1621
mesg_get_usernames: (mesg) =>
1622
if not @account_id?
1623
@error_to_client(id:mesg.id, error:"user must be signed in")
1624
return
1625
@database.get_usernames
1626
account_ids : mesg.account_ids
1627
use_cache : true
1628
cb : (err, usernames) =>
1629
if err
1630
@error_to_client(id:mesg.id, error:err)
1631
else
1632
@push_to_client(message.usernames(usernames:usernames, id:mesg.id))
1633
1634
###
1635
Support Tickets → Zendesk
1636
###
1637
mesg_create_support_ticket: (mesg) =>
1638
dbg = @dbg("mesg_create_support_ticket")
1639
dbg("#{misc.to_json(mesg)}")
1640
1641
m = underscore.omit(mesg, 'id', 'event')
1642
get_support().create_ticket m, (err, url) =>
1643
dbg("callback being called with #{err} and url: #{url}")
1644
if err?
1645
@error_to_client(id:mesg.id, error:err)
1646
else
1647
@push_to_client(
1648
message.support_ticket_url(id:mesg.id, url: url))
1649
1650
mesg_get_support_tickets: (mesg) =>
1651
# retrieves the support tickets the user with the current account_id
1652
dbg = @dbg("mesg_get_support_tickets")
1653
dbg("#{misc.to_json(mesg)}")
1654
if not @account_id
1655
err = "You must be signed in to use support related functions."
1656
@error_to_client(id:mesg.id, error:err)
1657
return
1658
1659
get_support().get_support_tickets @account_id, (err, tickets) =>
1660
if err?
1661
@error_to_client(id:mesg.id, error:err)
1662
else
1663
dbg("tickets: #{misc.to_json(tickets)}")
1664
@push_to_client(
1665
message.support_tickets(id:mesg.id, tickets: tickets))
1666
1667
###
1668
Stripe-integration billing code
1669
###
1670
ensure_fields: (mesg, fields) =>
1671
if not mesg.id?
1672
return false
1673
if typeof(fields) == 'string'
1674
fields = fields.split(' ')
1675
for f in fields
1676
if not mesg[f.trim()]?
1677
err = "invalid message; must have #{f} field"
1678
@error_to_client(id:mesg.id, error:err)
1679
return false
1680
return true
1681
1682
stripe_get_customer_id: (id, cb) => # id = message id
1683
# cb(err, customer_id)
1684
# - if err, then an error message with id the given id is sent to the
1685
# user, so client code doesn't have to
1686
# - if no customer info yet with stripe, then NOT an error; instead,
1687
# customer_id is undefined.
1688
dbg = @dbg("stripe_get_customer_id")
1689
dbg()
1690
if not @account_id?
1691
err = "You must be signed in to use billing related functions."
1692
@error_to_client(id:id, error:err)
1693
cb(err)
1694
return
1695
@_stripe = get_stripe()
1696
if not @_stripe?
1697
err = "stripe billing not configured"
1698
dbg(err)
1699
@error_to_client(id:id, error:err)
1700
cb(err)
1701
else
1702
if @stripe_customer_id?
1703
dbg("using cached @stripe_customer_id")
1704
cb(undefined, @stripe_customer_id)
1705
else
1706
if @_stripe_customer_id_cbs?
1707
@_stripe_customer_id_cbs.push({id:id, cb:cb})
1708
return
1709
@_stripe_customer_id_cbs = [{id:id, cb:cb}]
1710
dbg('getting stripe_customer_id from db...')
1711
@database.get_stripe_customer_id
1712
account_id : @account_id
1713
cb : (err, customer_id) =>
1714
@stripe_customer_id = customer_id # cache for later
1715
for x in @_stripe_customer_id_cbs
1716
{id, cb} = x
1717
if err
1718
dbg("fail -- #{err}")
1719
@error_to_client(id:id, error:err)
1720
cb(err)
1721
else
1722
dbg("got result #{customer_id}")
1723
cb(undefined, customer_id)
1724
delete @_stripe_customer_id_cbs
1725
1726
stripe_need_customer_id: (id, cb) =>
1727
# Like stripe_get_customer_id, except sends an error to the
1728
# user if they aren't registered yet, instead of returning undefined.
1729
@dbg("stripe_need_customer_id")()
1730
@stripe_get_customer_id id, (err, customer_id) =>
1731
if err
1732
cb(err); return
1733
if not customer_id?
1734
err = "customer not defined"
1735
@stripe_error_to_client(id:id, error:err)
1736
cb(err); return
1737
cb(undefined, customer_id)
1738
1739
stripe_get_customer: (id, cb) =>
1740
dbg = @dbg("stripe_get_customer")
1741
dbg("getting id")
1742
@stripe_get_customer_id id, (err, customer_id) =>
1743
if err
1744
dbg("failed -- #{err}")
1745
cb(err)
1746
return
1747
if not customer_id?
1748
dbg("no customer_id set yet")
1749
cb(undefined, undefined)
1750
return
1751
dbg("now getting stripe customer object")
1752
@_stripe.customers.retrieve customer_id, (err, customer) =>
1753
if err
1754
dbg("failed -- #{err}")
1755
@error_to_client(id:id, error:err)
1756
cb(err)
1757
else
1758
dbg("got it")
1759
cb(undefined, customer)
1760
1761
stripe_error_to_client: (opts) =>
1762
opts = defaults opts,
1763
id : required
1764
error : required
1765
err = opts.error
1766
if typeof(err) != 'string'
1767
if err.stack?
1768
err = err.stack.split('\n')[0]
1769
else
1770
err = misc.to_json(err)
1771
@dbg("stripe_error_to_client")(err)
1772
@error_to_client(id:opts.id, error:err)
1773
1774
mesg_stripe_get_customer: (mesg) =>
1775
dbg = @dbg("mesg_stripe_get_customer")
1776
dbg("get information from stripe about this customer, e.g., subscriptions, payment methods, etc.")
1777
@stripe_get_customer mesg.id, (err, customer) =>
1778
if err
1779
return
1780
resp = message.stripe_customer
1781
id : mesg.id
1782
stripe_publishable_key : @_stripe?.publishable_key
1783
customer : customer
1784
@push_to_client(resp)
1785
1786
mesg_stripe_create_source: (mesg) =>
1787
dbg = @dbg("mesg_stripe_get_customer")
1788
dbg("create a payment method (credit card) in stripe for this user")
1789
if not @ensure_fields(mesg, 'token')
1790
dbg("missing token field -- bailing")
1791
return
1792
dbg("looking up customer")
1793
@stripe_get_customer_id mesg.id, (err, customer_id) =>
1794
if err # database or other major error (e.g., no stripe conf)
1795
# @get_stripe_customer sends error message to user
1796
dbg("failed -- #{err}")
1797
return
1798
if not customer_id?
1799
dbg("create new stripe customer (from card token)")
1800
description = undefined
1801
email = undefined
1802
async.series([
1803
(cb) =>
1804
dbg("get identifying info about user")
1805
@database.get_account
1806
columns : ['email_address', 'first_name', 'last_name']
1807
account_id : @account_id
1808
cb : (err, r) =>
1809
if err
1810
cb(err)
1811
else
1812
email = r.email_address
1813
description = "#{r.first_name} #{r.last_name}"
1814
dbg("they are #{description} with email #{email}")
1815
cb()
1816
(cb) =>
1817
dbg("creating stripe customer")
1818
x =
1819
source : mesg.token
1820
description : description
1821
email : email
1822
metadata :
1823
account_id : @account_id
1824
@_stripe.customers.create x, (err, customer) =>
1825
if err
1826
cb(err)
1827
else
1828
customer_id = customer.id
1829
cb()
1830
(cb) =>
1831
dbg("success; now save customer id token to database")
1832
@database.set_stripe_customer_id
1833
account_id : @account_id
1834
customer_id : customer_id
1835
cb : cb
1836
(cb) =>
1837
dbg("success; sync user account with stripe")
1838
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
1839
], (err) =>
1840
if err
1841
dbg("failed -- #{err}")
1842
@stripe_error_to_client(id:mesg.id, error:err)
1843
else
1844
@success_to_client(id:mesg.id)
1845
)
1846
else
1847
dbg("add card to existing stripe customer")
1848
async.series([
1849
(cb) =>
1850
@_stripe.customers.createCard(customer_id, {card:mesg.token}, cb)
1851
(cb) =>
1852
dbg("success; sync user account with stripe")
1853
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
1854
], (err) =>
1855
if err
1856
@stripe_error_to_client(id:mesg.id, error:err)
1857
else
1858
@success_to_client(id:mesg.id)
1859
)
1860
1861
mesg_stripe_delete_source: (mesg) =>
1862
dbg = @dbg("mesg_stripe_delete_source")
1863
dbg("delete a payment method for this user")
1864
if not @ensure_fields(mesg, 'card_id')
1865
dbg("missing card_id field")
1866
return
1867
customer_id = undefined
1868
async.series([
1869
(cb) =>
1870
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
1871
(cb) =>
1872
if not customer_id?
1873
cb("no customer information so can't delete source")
1874
else
1875
@_stripe.customers.deleteCard(customer_id, mesg.card_id, cb)
1876
(cb) =>
1877
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
1878
], (err) =>
1879
if err
1880
@stripe_error_to_client(id:mesg.id, error:err)
1881
else
1882
@success_to_client(id:mesg.id)
1883
)
1884
1885
mesg_stripe_set_default_source: (mesg) =>
1886
dbg = @dbg("mesg_stripe_set_default_source")
1887
dbg("set a payment method for this user to be the default")
1888
if not @ensure_fields(mesg, 'card_id')
1889
dbg("missing field card_id")
1890
return
1891
customer_id = undefined
1892
async.series([
1893
(cb) =>
1894
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
1895
(cb) =>
1896
if not customer_id?
1897
cb("no customer information so can't update source")
1898
else
1899
dbg("now setting the default source in stripe")
1900
@_stripe.customers.update(customer_id, {default_source:mesg.card_id}, cb)
1901
(cb) =>
1902
dbg("update database")
1903
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
1904
], (err) =>
1905
if err
1906
dbg("failed -- #{err}")
1907
@stripe_error_to_client(id:mesg.id, error:err)
1908
else
1909
dbg("success")
1910
@success_to_client(id:mesg.id)
1911
)
1912
1913
mesg_stripe_update_source: (mesg) =>
1914
dbg = @dbg("mesg_stripe_update_source")
1915
dbg("modify a payment method")
1916
1917
if not @ensure_fields(mesg, 'card_id info')
1918
return
1919
if mesg.info.metadata?
1920
@error_to_client(id:mesg.id, error:"you may not change card metadata")
1921
return
1922
customer_id = undefined
1923
async.series([
1924
(cb) =>
1925
@stripe_get_customer_id(mesg.id, (err, x) => customer_id = x; cb(err))
1926
(cb) =>
1927
if not customer_id?
1928
cb("no customer information so can't update source")
1929
else
1930
@_stripe.customers.updateCard(customer_id, mesg.card_id, mesg.info, cb)
1931
(cb) =>
1932
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
1933
], (err) =>
1934
if err
1935
@stripe_error_to_client(id:mesg.id, error:err)
1936
else
1937
@success_to_client(id:mesg.id)
1938
)
1939
1940
mesg_stripe_get_plans: (mesg) =>
1941
dbg = @dbg("mesg_stripe_get_plans")
1942
dbg("get descriptions of the available plans that the user might subscribe to")
1943
async.series([
1944
(cb) =>
1945
@stripe_get_customer_id(mesg.id, cb) # ensures @_stripe is defined below
1946
(cb) =>
1947
@_stripe.plans.list (err, plans) =>
1948
if err
1949
@stripe_error_to_client(id:mesg.id, error:err)
1950
else
1951
@push_to_client(message.stripe_plans(id: mesg.id, plans: plans))
1952
])
1953
1954
mesg_stripe_create_subscription: (mesg) =>
1955
dbg = @dbg("mesg_stripe_create_subscription")
1956
dbg("create a subscription for this user, using some billing method")
1957
if not @ensure_fields(mesg, 'plan')
1958
@stripe_error_to_client(id:mesg.id, error:"missing field 'plan'")
1959
return
1960
1961
schema = require('smc-util/schema').PROJECT_UPGRADES.membership[mesg.plan.split('-')[0]]
1962
if not schema?
1963
@stripe_error_to_client(id:mesg.id, error:"unknown plan -- '#{mesg.plan}'")
1964
return
1965
1966
@stripe_need_customer_id mesg.id, (err, customer_id) =>
1967
if err
1968
dbg("fail -- #{err}")
1969
return
1970
projects = mesg.projects
1971
if not mesg.quantity?
1972
mesg.quantity = 1
1973
1974
options =
1975
plan : mesg.plan
1976
quantity : mesg.quantity
1977
coupon : mesg.coupon
1978
1979
subscription = undefined
1980
tax_rate = undefined
1981
async.series([
1982
(cb) =>
1983
dbg('determine applicable tax')
1984
require('./stripe/sales-tax').stripe_sales_tax
1985
customer_id : customer_id
1986
cb : (err, rate) =>
1987
tax_rate = rate
1988
dbg("tax_rate = #{tax_rate}")
1989
if tax_rate
1990
# CRITICAL: if we don't just multiply by 100, since then sometimes
1991
# stripe comes back with an error like this
1992
# "Error: Invalid decimal: 8.799999999999999; must contain at maximum two decimal places."
1993
options.tax_percent = Math.round(tax_rate*100*100)/100
1994
cb(err)
1995
(cb) =>
1996
dbg("add customer subscription to stripe")
1997
@_stripe.customers.createSubscription customer_id, options, (err, s) =>
1998
if err
1999
cb(err)
2000
else
2001
subscription = s
2002
cb()
2003
(cb) =>
2004
if schema.cancel_at_period_end
2005
dbg("Setting subscription to cancel at period end")
2006
@_stripe.customers.cancelSubscription(customer_id, subscription.id, {at_period_end:true}, cb)
2007
else
2008
cb()
2009
(cb) =>
2010
dbg("Successfully added subscription; now save info in our database about subscriptions....")
2011
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
2012
], (err) =>
2013
if err
2014
dbg("fail -- #{err}")
2015
@stripe_error_to_client(id:mesg.id, error:err)
2016
else
2017
@success_to_client(id:mesg.id)
2018
)
2019
2020
mesg_stripe_cancel_subscription: (mesg) =>
2021
dbg = @dbg("mesg_stripe_cancel_subscription")
2022
dbg("cancel a subscription for this user")
2023
if not @ensure_fields(mesg, 'subscription_id')
2024
dbg("missing field subscription_id")
2025
return
2026
@stripe_need_customer_id mesg.id, (err, customer_id) =>
2027
if err
2028
return
2029
projects = undefined
2030
subscription_id = mesg.subscription_id
2031
async.series([
2032
(cb) =>
2033
dbg("cancel the subscription at stripe")
2034
# This also returns the subscription, which lets
2035
# us easily get the metadata of all projects associated to this subscription.
2036
@_stripe.customers.cancelSubscription(customer_id, subscription_id, {at_period_end:mesg.at_period_end}, cb)
2037
(cb) =>
2038
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
2039
], (err) =>
2040
if err
2041
@stripe_error_to_client(id:mesg.id, error:err)
2042
else
2043
@success_to_client(id:mesg.id)
2044
)
2045
2046
mesg_stripe_update_subscription: (mesg) =>
2047
dbg = @dbg("mesg_stripe_update_subscription")
2048
dbg("edit a subscription for this user")
2049
if not @ensure_fields(mesg, 'subscription_id')
2050
dbg("missing field subscription_id")
2051
return
2052
subscription_id = mesg.subscription_id
2053
@stripe_need_customer_id mesg.id, (err, customer_id) =>
2054
if err
2055
return
2056
subscription = undefined
2057
async.series([
2058
(cb) =>
2059
dbg("Update the subscription.")
2060
changes =
2061
quantity : mesg.quantity
2062
plan : mesg.plan
2063
coupon : mesg.coupon
2064
@_stripe.customers.updateSubscription(customer_id, subscription_id, changes, cb)
2065
(cb) =>
2066
@database.stripe_update_customer(account_id : @account_id, stripe : @_stripe, customer_id : customer_id, cb: cb)
2067
], (err) =>
2068
if err
2069
@stripe_error_to_client(id:mesg.id, error:err)
2070
else
2071
@success_to_client(id:mesg.id)
2072
)
2073
2074
mesg_stripe_get_subscriptions: (mesg) =>
2075
dbg = @dbg("mesg_stripe_get_subscriptions")
2076
dbg("get a list of all the subscriptions that this customer has")
2077
@stripe_need_customer_id mesg.id, (err, customer_id) =>
2078
if err
2079
return
2080
options =
2081
limit : mesg.limit
2082
ending_before : mesg.ending_before
2083
starting_after : mesg.starting_after
2084
@_stripe.customers.listSubscriptions customer_id, options, (err, subscriptions) =>
2085
if err
2086
@stripe_error_to_client(id:mesg.id, error:err)
2087
else
2088
@push_to_client(message.stripe_subscriptions(id:mesg.id, subscriptions:subscriptions))
2089
2090
mesg_stripe_get_charges: (mesg) =>
2091
dbg = @dbg("mesg_stripe_get_charges")
2092
dbg("get a list of charges for this customer.")
2093
@stripe_need_customer_id mesg.id, (err, customer_id) =>
2094
if err
2095
return
2096
options =
2097
customer : customer_id
2098
limit : mesg.limit
2099
ending_before : mesg.ending_before
2100
starting_after : mesg.starting_after
2101
@_stripe.charges.list options, (err, charges) =>
2102
if err
2103
@stripe_error_to_client(id:mesg.id, error:err)
2104
else
2105
@push_to_client(message.stripe_charges(id:mesg.id, charges:charges))
2106
2107
mesg_stripe_get_invoices: (mesg) =>
2108
dbg = @dbg("mesg_stripe_get_invoices")
2109
dbg("get a list of invoices for this customer.")
2110
@stripe_need_customer_id mesg.id, (err, customer_id) =>
2111
if err
2112
return
2113
options =
2114
customer : customer_id
2115
limit : mesg.limit
2116
ending_before : mesg.ending_before
2117
starting_after : mesg.starting_after
2118
@_stripe.invoices.list options, (err, invoices) =>
2119
if err
2120
@stripe_error_to_client(id:mesg.id, error:err)
2121
else
2122
@push_to_client(message.stripe_invoices(id:mesg.id, invoices:invoices))
2123
2124
mesg_stripe_admin_create_invoice_item: (mesg) =>
2125
if not @user_is_in_group('admin')
2126
@error_to_client(id:mesg.id, error:"must be logged in and a member of the admin group to create invoice items")
2127
return
2128
dbg = @dbg("mesg_stripe_admin_create_invoice_item")
2129
@_stripe = get_stripe()
2130
if not @_stripe?
2131
err = "stripe billing not configured"
2132
dbg(err)
2133
@error_to_client(id:id, error:err)
2134
return
2135
customer_id = undefined
2136
description = undefined
2137
email = undefined
2138
new_customer = true
2139
async.series([
2140
(cb) =>
2141
dbg("check for existing stripe customer_id")
2142
@database.get_account
2143
columns : ['stripe_customer_id', 'email_address', 'first_name', 'last_name', 'account_id']
2144
account_id : mesg.account_id
2145
email_address : mesg.email_address
2146
cb : (err, r) =>
2147
if err
2148
cb(err)
2149
else
2150
customer_id = r.stripe_customer_id
2151
email = r.email_address
2152
description = "#{r.first_name} #{r.last_name}"
2153
mesg.account_id = r.account_id
2154
cb()
2155
(cb) =>
2156
if customer_id?
2157
new_customer = false
2158
dbg("already signed up for stripe -- sync local user account with stripe")
2159
@database.stripe_update_customer
2160
account_id : mesg.account_id
2161
stripe : get_stripe()
2162
customer_id : customer_id
2163
cb : cb
2164
else
2165
dbg("create stripe entry for this customer")
2166
x =
2167
description : description
2168
email : email
2169
metadata :
2170
account_id : mesg.account_id
2171
@_stripe.customers.create x, (err, customer) =>
2172
if err
2173
cb(err)
2174
else
2175
customer_id = customer.id
2176
cb()
2177
(cb) =>
2178
if not new_customer
2179
cb()
2180
else
2181
dbg("store customer id in our database")
2182
@database.set_stripe_customer_id
2183
account_id : mesg.account_id
2184
customer_id : customer_id
2185
cb : cb
2186
(cb) =>
2187
if not (mesg.amount? and mesg.description?)
2188
dbg("no amount or description -- not creating an invoice")
2189
cb()
2190
else
2191
dbg("now create the invoice item")
2192
@_stripe.invoiceItems.create
2193
customer : customer_id
2194
amount : mesg.amount*100
2195
currency : "usd"
2196
description : mesg.description
2197
,
2198
(err, invoice_item) =>
2199
if err
2200
cb(err)
2201
else
2202
cb()
2203
], (err) =>
2204
if err
2205
@error_to_client(id:mesg.id, error:err)
2206
else
2207
@success_to_client(id:mesg.id)
2208
)
2209
2210
2211
mesg_api_key: (mesg) =>
2212
api_key_action
2213
database : @database
2214
account_id : @account_id
2215
password : mesg.password
2216
action : mesg.action
2217
cb : (err, api_key) =>
2218
if err
2219
@error_to_client(id:mesg.id, error:err)
2220
else
2221
if api_key?
2222
@push_to_client(message.api_key_info(id:mesg.id, api_key:api_key))
2223
else
2224
@success_to_client(id:mesg.id)
2225
2226
mesg_user_auth: (mesg) =>
2227
auth_token.get_user_auth_token
2228
database : @database
2229
account_id : @account_id # strictly not necessary yet... but good if user has to be signed in,
2230
# since more secure and we can rate limit attempts from a given user.
2231
user_account_id : mesg.account_id
2232
password : mesg.password
2233
cb : (err, auth_token) =>
2234
if err
2235
@error_to_client(id:mesg.id, error:err)
2236
else
2237
@push_to_client(message.user_auth_token(id:mesg.id, auth_token:auth_token))
2238
2239
mesg_revoke_auth_token: (mesg) =>
2240
auth_token.revoke_user_auth_token
2241
database : @database
2242
auth_token : mesg.auth_token
2243
cb : (err) =>
2244
if err
2245
@error_to_client(id:mesg.id, error:err)
2246
else
2247
@push_to_client(message.success(id:mesg.id))
2248
2249