'use strict';
/*
* https://github.com/adityamukho/node-box-sdk
*
* Copyright (c) 2014 Aditya Mukhopadhyay
* Licensed under the MIT license.
*/
/**
* The Box SDK entry point.
* @example
* var box_sdk = require('box-sdk');
* @module box-sdk
*/
/**
* A callback with an optional error argument.
* @callback optionalErrorCallback
* @param {Error} [error] - Any error that occurred while attempting to stop the server.
*/
/**
* A Writable Stream.
* @external Writable
* @see {@link http://nodejs.org/api/stream.html#stream_class_stream_writable}
*/
var base = require('base-framework'),
_ = require('lodash'),
Log = require('log'),
Connection = require('./connector');
/**
* Constructor options for the Box object.
* @typedef {Object} BoxInit
* @property {string} client_id - The Box app's Client ID.
* @property {string} client_secret - The Box app's Client Secret.
* @property {number} port - The port on which to listen for the authorization callback.
* @property {string} [host] - Optional host on which to listen for the authorization callback.
* Defaults to {@linkcode localhost}.
*/
/**
* @class Box
* @classdesc The Box object: One instance for each client. This is used to get {@link Connection|connections}.
* @param {?BoxInit} [opts] - Client parameters required for standalone operation. Should be omitted when
* running with [passport authentication middleware]{@link https://github.com/bluedge/passport-box}.
* @param {?string} [logLevel] - Optional log level. Defaults to {@linkcode info}.
* @param {?external:Writable} [logStream] - Optional writable stream for log output. Defaults to console.
* @see {@link https://www.npmjs.org/package/log#log-levels}
*/
var Box = base.createChild().addInstanceMethods(
/** @lends Box.prototype */
{
init: function (opts, logLevel, logStream) {
var self = this;
self.connections = {};
if (opts) {
if (!opts.client_id) {
throw new Error('Must specify a client_id');
}
if (!opts.client_secret) {
throw new Error('Must specify a client_secret');
}
self.port = parseInt(opts.port, 10);
if (!_.isNumber(self.port)) {
throw new Error('Must specify a numeric port');
}
self.host = opts.host || 'localhost';
self.log = new Log(logLevel || 'info', logStream);
self.client_id = opts.client_id;
self.client_secret = opts.client_secret;
var router = require('router'),
http = require('http'),
request = require('request'),
url = require('url');
var route = router();
route.get('/authorize', function (req, res) {
var params = url.parse(req.url, true).query;
if (!self.connections[params.id]) {
params.error = 'invalid_request';
params.error_description = "Could not identify a connection for this request.";
} else if (params.state !== self.connections[params.id].csrf) {
params.error = 'forged_request';
params.error_description = "This looks like a CSRF attack. Someone may be trying to access your account illegaly.";
}
if (params.error) {
var message = params.error_description || "Unknown error";
self.log.error(
"Error authenticating user - Error Code: %s, Error Description: %s",
params.error, message
);
var ecode;
switch (params.error) {
case 'access_denied':
case 'forged_request':
ecode = 401;
break;
case 'invalid_request':
case 'unsupported_response_type':
ecode = 400;
break;
case 'server_error':
ecode = 500;
break;
}
res.writeHead(ecode, message);
res.end('Error ' + ecode + ': ' + message);
} else {
res.writeHead(200);
res.end("Authorization code received.");
var tokenParams = {
client_id: self.client_id,
client_secret: self.client_secret,
grant_type: 'authorization_code',
code: params.code
},
authUrl = 'https://www.box.com/api/oauth2/token';
request.post({
url: authUrl,
form: tokenParams,
json: true
}, function (err, res, body) {
if (err) {
/**
* Fires when an error occurs while setting access tokens have been set on this
* connection. Could be triggered more than once, so listeners must deregister
* after receiving the first event. Preferably use the {@link Connection#ready} method.
* @event Connection#"tokens.error"
* @type {Error}
* @see {@link Connection#ready}
*/
self.connections[params.id].emit('tokens.error', err);
} else {
self.connections[params.id]._setTokens(body);
}
});
}
});
self.server = http.createServer(route);
self.server.listen(self.port, self.host, function () {
self.log.info('Box SDK listening at http://%s:%d', self.host, self.port);
});
//Track connections
self.sockets = {};
self.server.on('connection', function (socket) {
self.sockets[socket.fd] = socket;
socket.on('close', function () {
delete self.sockets[socket.fd];
});
});
}
},
/**
* Get a connection for the provided email id.
* @param {string} email - The email account identifier to connect to.
* @returns {Connection} A connection on which API calls can be made.
* @fires Connection#"tokens.set"
*/
getConnection: function (email) {
if (this.connections[email]) {
return this.connections[email];
}
var connection = new Connection(this, email);
this.connections[email] = connection;
return connection;
},
/**
* Used with Passport {@link https://github.com/bluedge/passport-box|BoxStrategy}.
* @returns {function} A callback for use in the BoxStrategy.
* @example
* passport.use(new BoxStrategy({
* clientID: BOX_CLIENT_ID,
* clientSecret: BOX_CLIENT_SECRET,
* callbackURL: "http://127.0.0.1:3000/auth/box/callback"
* }, box.authenticate()));
*/
authenticate: function () {
return _.bind(function (access_token, refresh_token, profile, done) {
var email = profile.login,
connection = this.getConnection(email);
connection._setTokens({
access_token: access_token,
refresh_token: refresh_token
});
return done(null, profile);
}, this);
},
/**
* Stop the authorization callback listener when running in standalone mode.
* @param {optionalErrorCallback} callback - Called after asynchronously stopping the server.
*/
stopServer: function (callback) {
if (this.server) {
this.log.debug('Stopping server...');
try {
this.server.close();
_.forIn(this.sockets, function (socket) {
socket.destroy();
});
callback();
} catch (err) {
callback(err);
}
}
}
});
/**
* The {@link Box} prototype, instances of which represent a specific client.
* @example
* var box = box_sdk.Box({
* client_id: 'client id',
* client_secret: 'client secret',
* port: 9999
* });
*/
exports.Box = Box;
/**
* The {@link Connection} prototype, instances of which represent a specific account.
*/
exports.Connection = Connection;