1 /**
  2  * @fileOverview API class definition
  3  */
  4 
  5 var Resource = require('./api/resource')
  6   , Account = require('./api/account')
  7   , Billing = require('./api/billing')
  8   , Channel = require('./api/channel')
  9   , Message = require('./api/message')
 10   , Session = require('./api/session')
 11   , Subscription = require('./api/subscription')
 12   , Application = require('./api/application')
 13   , Member = require('./api/member')
 14   ;
 15 
 16 /**
 17  * Abstraction for the Spire API
 18  *
 19  * @class Collection of API methods
 20  *
 21  * @example
 22  * var api = new API(options);
 23  *
 24  * @constructor
 25  * @param {object} spire Spire object
 26  * @param {object} [opts] Options
 27  * @param {string} [opts.url] Spire api url
 28  * @param {string} [opts.version] Version of the Spire api to use
 29  */
 30 function API(spire, opts) {
 31   /**
 32    * Reference to spire object.
 33    */
 34   this.spire = spire;
 35 
 36   opts = opts || {};
 37 
 38   /**
 39    * URL or spire.io API.
 40    * Defaults to 'https://api.spire.io'
 41    */
 42   this.url = opts.url || 'https://api.spire.io';
 43 
 44   /**
 45    * Version of spire.io API to use.
 46    * Defaults to '1.0'
 47    */
 48   this.version = opts.version || '1.0';
 49 
 50   /**
 51    * Schema definition for spire API.
 52    */
 53   this.schema = null;
 54 }
 55 
 56 module.exports = API;
 57 
 58 
 59 /**
 60  * Make requests to the api.
 61  * @function
 62  * @see Resourse.prototype.request
 63  */
 64 API.prototype.request = Resource.prototype.request;
 65 
 66 /**
 67  * Discovers urls from Spire API.  Since this description does not change often,
 68  * we only make the request once and cache the result for subsequent calls.
 69  *
 70  * @example
 71  * api.discover(function (err, discovered) {
 72  *   if (!err) {
 73  *     // ...
 74  *   }
 75  * });
 76  *
 77  * @param {function(err, discovered)} cb Callback
 78  */
 79 API.prototype.discover = function (cb) {
 80   var api = this;
 81 
 82   if (this.description) {
 83     return process.nextTick(function () {
 84       cb(null, api.description);
 85     });
 86   }
 87 
 88   this.request('discover', function (err, description) {
 89     if (err) return cb(err);
 90     api.description = description;
 91     api.schema = description.schema[api.version];
 92     cb(null, description);
 93   });
 94 };
 95 
 96 /**
 97  * Creates a spire session from an account secret.
 98  *
 99  * @param {string} secret The acccount secret
100  * @param {function(err)} cb Callback
101  */
102 API.prototype.createSession = function (secret, cb) {
103   var api = this;
104   this.discover(function (err) {
105     if (err) return cb(err);
106     api.request('create_session', secret, function (err, sessionData) {
107       if (err) return cb(err);
108       var session = new Session(api.spire, sessionData);
109       cb(null, session);
110     });
111   });
112 };
113 
114 /**
115  * Logs in with the given email and password.
116  *
117  * @param {string} email Email
118  * @param {string} password Password
119  * @param {function(err)} cb Callback
120  */
121 API.prototype.login = function (email, password, cb) {
122   var api = this;
123   this.discover(function (err) {
124     if (err) return cb(err);
125     api.request('login', email, password, function (err, sessionData) {
126       if (err) return cb(err);
127       var session = new Session(api.spire, sessionData);
128       cb(null, session);
129     });
130   });
131 };
132 
133 /**
134  * Register for a new spire account, and authenticates as the newly created account
135  *
136  * @param {object} user User info
137  * @param {string} user.email Email
138  * @param {string} user.password Password
139  * @param {string} [user.password_confirmation] Optional password confirmation
140  * @param {function (err)} cb Callback
141  */
142 API.prototype.createAccount = function (info, cb) {
143   var api = this;
144   this.discover(function (err) {
145     if (err) return cb(err);
146     api.request('create_account', info, function (err, sessionData) {
147       if (err) return cb(err);
148       var session = new Session(api.spire, sessionData);
149       cb(null, session);
150     });
151   });
152 };
153 
154 /**
155  * Request a password reset for email.
156  *
157  * @param {string} email Email
158  * @param {function (err)} cb Callback
159  */
160 API.prototype.passwordResetRequest = function (email, cb) {
161   var api = this;
162   this.discover(function (err) {
163     if (err) return cb(err);
164     api.request('password_reset', email, cb);
165   });
166 };
167 
168 /**
169  * Get billing information for the account.
170  *
171  * @param {function (err, billingResource)} cb Callback
172  */
173 API.prototype.billing = function (cb) {
174   var api = this;
175   this.discover(function (err) {
176     if (err) return cb(err);
177     api.request('billing', function (err, billingData) {
178       if (err) return cb(err);
179       var billing = new Billing(api.spire, billingData);
180       cb(null, billing);
181     });
182   });
183 };
184 
185 /**
186  * Get Account from url and capabilities.
187  *
188  * Use this method to get the account without starting a spire session.
189  *
190  * If you have a spire session, you should use <code>spire.session.account</code>.
191  *
192  * @example
193  * var spire = new Spire();
194  * spire.api.accountFromUrlAndCapabilities({
195  *   url: account_url,
196  *   capabilities: account_capabilities
197  * }, function (err, account) {
198  *   if (!err) {
199  *     // ...
200  *   }
201  * })
202  *
203  * @param {object} creds Url and Capabilities
204  * @param {string} creds.url Url
205  * @param {string} creds.capabilities Capabilities
206  * @param {function (err, account)} cb Callback
207  */
208 API.prototype.accountFromUrlAndCapabilities = function (creds, cb) {
209   var api = this;
210   this.discover(function (err) {
211     if (err) return cb(err);
212     var account = new Account(api.spire, creds);
213     account.get(cb);
214   });
215 };
216 
217 /**
218  * Update Account from url and capability.
219  *
220  * @param {object} account Must contain at least Url and Capability
221  * @param {string} creds.url Url
222  * @param {string} creds.capability Capability
223  * @param {function (err, account)} cb Callback
224  */
225 API.prototype.updateAccountWithUrlAndCapability = function (accountData, cb) {
226   var api = this;
227   this.discover(function (err) {
228     if (err) return cb(err);
229     api.request('update_account', accountData, function (err, acc) {
230       if (err) return cb(err);
231       var account = new Account(api.spire, acc);
232       cb(null, account);
233     });
234   });
235 };
236 
237 /**
238  * Get Channel from url and capabilities.
239  *
240  * Use this method to get a channel without starting a spire session.
241  *
242  * If you have a spire session, you should use <code>spire.channel</code>.
243  *
244  * @example
245  * var spire = new Spire();
246  * spire.api.channelFromUrlAndCapabilities({
247  *   url: channel_url,
248  *   capabilities: channel_capabilities
249  * }, function (err, channel) {
250  *   if (!err) {
251  *     // ...
252  *   }
253  * })
254  *
255  * @param {object} creds Url and Capabilities
256  * @param {string} creds.url Url
257  * @param {string} creds.capabilities Capabilities
258  * @param {function (err, channel)} cb Callback
259  */
260 API.prototype.channelFromUrlAndCapabilities = function (creds, cb) {
261   var api = this;
262   this.discover(function (err) {
263     if (err) return cb(err);
264     var channel = new Channel(api.spire, creds);
265     channel.getIfCapable(cb);
266   });
267 };
268 
269 /**
270  * Get Session from url and capabilities.
271  *
272  * @param {object} creds Url and Capabilities
273  * @param {string} creds.url Url
274  * @param {string} creds.capabilities Capabilities
275  * @param {function (err, subscription)} cb Callback
276  */
277 API.prototype.sessionFromUrlAndCapabilities = function (creds, cb) {
278   var api = this;
279   this.discover(function (err) {
280     if (err) return cb(err);
281     var session = new Session(api.spire, creds);
282     api.spire.session = session;
283     session.getIfCapable(cb);
284   });
285 };
286 
287 /**
288  * Get Subscription from url and capabilities.
289  *
290  * Use this method to get a subscription without starting a spire session.
291  *
292  * If you have a spire session, you should use <code>spire.subscription</code>.
293  *
294  * @example
295  * var spire = new Spire();
296  * spire.api.subscriptionFromUrlAndCapabilities({
297  *   url: subscription_url,
298  *   capabilities: subscription_capabilities
299  * }, function (err, subscription) {
300  *   if (!err) {
301  *     // ...
302  *   }
303  * })
304  *
305  * @param {object} creds Url and Capabilities
306  * @param {string} creds.url Url
307  * @param {string} creds.capabilities Capabilities
308  * @param {function (err, subscription)} cb Callback
309  */
310 API.prototype.subscriptionFromUrlAndCapabilities = function (creds, cb) {
311   var api = this;
312   this.discover(function (err) {
313     if (err) return cb(err);
314     var subscription = new Subscription(api.spire, creds);
315     process.nextTick(function () {
316       cb(null, subscription);
317     });
318   });
319 };
320 
321 /**
322  * Returns the MIME type for resourceName.
323  *
324  * @param {string} [name] Name of the resource MIME type to return
325  * @returns {string} MIME type of the resource
326  */
327 API.prototype.mediaType = function (resourceName) {
328   if (!this.schema) {
329     throw "No description object.  Run `spire.api.discover` first.";
330   }
331 
332   if (!this.schema[resourceName]) {
333     throw "No schema for resource " + resourceName;
334   }
335 
336   return this.schema[resourceName].mediaType;
337 };
338 
339 /**
340  * Returns the Authorization header for the resource and method.
341  *
342  * @param {Resource} resource Resource
343  * @param {string} method Method
344  * @returns {string} Authorization header for the resource
345  */
346 API.prototype.authorization = function (method, resource) {
347   return ['Capability', resource.capabilities[method]].join(' ');
348 };
349 
350 /**
351  * Returns an application when passed an application key.
352  *
353  * @param {string} key Application key
354  * @param {function(err)} cb Callback
355  */
356 API.prototype.getApplication = function (key, cb) {
357   var api = this;
358   this.discover(function (err) {
359     if (err) return cb(err);
360     api.request('get_application', key, function (err, appData) {
361       if (err) return cb(err);
362       var application = new Application(api.spire, appData);
363       cb(null, application);
364     });
365   });
366 };
367 
368 /**
369  * Requests
370  * These define API calls and have no side effects.  They can be run by calling
371  *     this.request(<request name>);
372  */
373 
374 /**
375  * Gets the api description resource.
376  * @name discover
377  * @ignore
378  */
379 Resource.defineRequest(API.prototype, 'discover', function () {
380   return {
381     method: 'get',
382     url: this.url,
383     headers: {
384       accept: "application/json"
385     }
386   };
387 });
388 
389 /**
390  * Posts to sessions url with the accont secret.
391  * @name create_session
392  * @ignore
393  */
394 Resource.defineRequest(API.prototype, 'create_session', function (secret) {
395   return {
396     method: 'post',
397     url: this.description.resources.sessions.url,
398     headers: {
399       'Content-Type': this.mediaType('account'),
400       'Accept': this.mediaType('session')
401     },
402     content: {secret: secret}
403   };
404 });
405 
406 /**
407  * Posts to sessions url with email and password.
408  * @name login
409  * @ignore
410  */
411 Resource.defineRequest(API.prototype, 'login', function (email, password) {
412   return {
413     method: 'post',
414     url: this.description.resources.sessions.url,
415     headers: {
416       'Content-Type': this.mediaType('account'),
417       'Accept': this.mediaType('session')
418     },
419     content: {
420       email: email,
421       password: password
422     }
423   };
424 });
425 
426 /**
427  * Posts to accounts url with user info.
428  * @name create_account
429  * @ignore
430  */
431 Resource.defineRequest(API.prototype, 'create_account', function (account) {
432   return {
433     method: 'post',
434     url: this.description.resources.accounts.url,
435     headers: {
436       'Content-Type': this.mediaType('account'),
437       'Accept': this.mediaType('session')
438     },
439     content: account
440   };
441 });
442 
443 /**
444  * Posts to accounts url with object containing email.
445  * @name password_reset
446  * @ignore
447  */
448 Resource.defineRequest(API.prototype, 'password_reset', function (email) {
449   return {
450     method: 'post',
451     url: this.description.resources.accounts.url,
452     content: "",
453     query: { email: email }
454   };
455 });
456 
457 /**
458  * Gets billing resource.
459  * @name billing
460  * @ignore
461  */
462 Resource.defineRequest(API.prototype, 'billing', function () {
463   return {
464     method: 'get',
465     url: this.description.resources.billing.url,
466     content: "",
467     headers: {
468       'Accept': 'application/json'
469     }
470   };
471 });
472 
473 /**
474  * Updates (puts) to the resouce.
475  * @name update
476  * @ignore
477  */
478 Resource.defineRequest(API.prototype, 'update_account', function (data) {
479   return {
480     method: 'put',
481     url: data.url,
482     content: data,
483     headers: {
484       'Authorization': "Capability " + data.capability,
485       'Accept': this.mediaType('account'),
486       'Content-Type': this.mediaType('account')
487     }
488   };
489 });
490 
491 /**
492  * Gets an application resource.
493  * @name application
494  * @ignore
495  */
496 Resource.defineRequest(API.prototype, 'get_application', function (app_key) {
497   return {
498     method: 'get',
499     url: this.description.resources.applications.url,
500     content: "",
501     query: { application_key: app_key },
502     headers: {
503       'Accept': this.mediaType('applications'),
504       'Content-Type': this.mediaType('applications')
505     }
506   };
507 });
508