Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First import with testing structure

  • Loading branch information...
commit 5bb9ae639f66dbd2738b386567e885d55729a081 1 parent ca373c3
@andreareginato authored
View
19 .gitignore
@@ -0,0 +1,19 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile ~/.gitignore_global
+
+# Ignore temporary files
+*.swp
+*.swo
+*.DS_Store
+
+# Ignore dependency libraries
+node_modules
+
+# Ignore logs
+*.log
+
+# Procfile env
+*.env
View
3  .npmignore
@@ -0,0 +1,3 @@
+.settings.xml
+.settings
+.c9revisions
View
5 CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## v0.1.0 ()
+
+First version Node client for OAuth2
View
1  LICENSE.md
@@ -0,0 +1 @@
+Copyright (c) 2013 Lelylan
View
15 Makefile
@@ -0,0 +1,15 @@
+MOCHA_OPTS=
+REPORTER = dot
+
+test:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --reporter $(REPORTER) \
+ $(MOCHA_OPTS)
+
+test-watch:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --reporter $(REPORTER) \
+ --growl \
+ --watch
+
+.PHONY: test test-watch
View
137 README.md
@@ -0,0 +1,137 @@
+# Lelylan API for Node.js
+
+Node.js client library for [Oauth2](http://oauth.net/2/)
+
+
+## Requirements
+
+Node client library is tested against Node ~0.8.x
+
+
+## Installation
+
+Install the client library using [npm](http://npmjs.org/):
+
+ $ npm install simple-oath2
+
+Install the client library using git:
+
+ $ git clone git://github.com/andrearegianto/simple-oauth2.git
+ $ cd simple-oauth2
+ $ npm install
+
+
+## Documentation
+
+* [Simple Oauth2 Docs](git://andreareginato.github.com/simple-oauth2)
+
+
+## Getting started
+
+### Get the Access Token
+
+```javascript
+var credentials = { client: { id: 'client-id', secret: 'client-secret', site: 'https://example.org' } };
+var OAuth2 = require('simple-oauth2')(credentials);
+
+// Returns the URI where to redirect your app
+var redirect = Oauth2.AuthCode.authorizeURL({ redirectURI: 'http://localhost:3000/callback', scope: 'user', state: '02afe928b');
+// => "https://example.org/oauth/authorization?response_type=code&client_id=client_id&redirect_uri=http://localhost:3000/callback&scope=user&state=02afe928b"
+
+// Get the access token object
+vat params = { code: 'authorization-code', redirectURI: 'http://localhost:3000/callback' }
+client.authCode.getToken(params, function(error, token){
+ // save the token object
+})
+```
+
+### Refresh the Access Token
+
+```javascript
+
+if (token.expired()) {
+ token.refresh(function(error, newToken) { token = newToken; })
+}
+```
+
+### Authorization Grants
+
+Currently the Authorization Code and Resource Owner Password Credentials grant types
+have helper strategy classes that simplify client use. They are available via the #authCode
+and #password methods respectively.
+
+// Authorization code flow
+var authURL = client.authCode.authorizeURL({ redirect_uri: 'http://localhost:3000/callback');
+var token = client.authCode.getToken({ code: 'authorization-code', redirectURI: 'http://localhost:3000/callback' }, callback);
+
+// Password credentials flow
+var token = client.password.getToken({ username: 'username', 'password': 'password' }, callback);
+
+If the functions fails an error object is passed as first argument to the callback.
+The body response object is always the last argument.
+
+## Errors
+
+Exceptions are raised when a 4xx or 5xx status code is returned.
+
+```javascript
+SimpleOAtuh2.Error
+```
+
+Through the error message attribute you can access the JSON representation.
+
+```javascript
+client.authCode.getToken(function(error, token) {
+ if (error) { console.log(error.message); }
+});
+```
+
+## Contributing
+
+Fork the repo on github and send a pull requests with topic branches. Do not forget to
+provide specs to your contribution.
+
+
+### Running specs
+
+* Fork and clone the repository (`dev` branch).
+* Run `npm install` for dependencies.
+* Run `make test` to execute all specs.
+* Run `make test-watch` to auto execute all specs when a file change.
+
+
+## Coding guidelines
+
+Follow [github](https://github.com/styleguide/) guidelines.
+
+
+## Feedback
+
+Use the [issue tracker](http://github.com/lelylan/lelylan-node/issues) for bugs.
+[Mail](mailto:touch@lelylan.com) or [Tweet](http://twitter.com/lelylan) us for any idea that can improve the project.
+
+
+## Links
+
+* [GIT Repository](http://github.com/andreareginato/simple-oauth2)
+* [Documentation](http://andreareginato.github.com/simple-oauth2)
+
+
+## Authors
+
+[Andrea Reginato](http://twitter.com/andreareginato)
+
+
+## Contributors
+
+Special thanks to the following people for submitting patches.
+
+
+## Changelog
+
+See [CHANGELOG](simple-oauth2/blob/master/CHANGELOG.md)
+
+
+## Copyright
+
+Copyright (c) 2013 [Lelylan](http://lelylan.com). See [LICENSE](simple-oauth2/blob/master/LICENSE.md) for details.
View
2  index.js
@@ -0,0 +1,2 @@
+// Convention to make this repo directly checked out into a project's deps folder
+module.exports = require('./lib/simple-oauth2');
View
31 lib/client/auth-code.js
@@ -0,0 +1,31 @@
+//
+// A class to implement the Authorization Code flow.
+//
+module.exports = function(config) {
+
+ var core = require('./../core')(config),
+ qs = require('querystring');
+
+ // Returns the OAuth2 authorization URI where the user decides to
+ // grant or deny the resources' access.
+ //
+ // * `redirectURI` - A String that represents the callback uri.
+ // * `scope` - A String that represents the application privileges.
+ // * `state` - A String that represents an optional opaque value used by the client to
+ // maintain state between the request and the callback.
+ // * `callback` - The callback function returning the results.
+ // An error object is passed as first argument and the result as last.
+ //
+ function authorizeURL(params) {
+ params.response_type = 'code';
+ params.client_id = config.client.id;
+
+ return config.client.site + config.authorizationPath + '?' + qs.stringify(params);
+ }
+
+ //core.api('GET', '/devices/' + id, {}, callback);
+
+ return {
+ 'authorizeURL' : authorizeURL,
+ }
+};
View
5 lib/config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ 'authorizationPath' : '/oauth/authorization',
+ 'tokenPath' : '/oauth/token',
+ 'client': {}
+}
View
91 lib/core.js
@@ -0,0 +1,91 @@
+var exports = module.exports,
+ request = require('request'),
+ crypto = require('crypto'),
+ util = require("util");
+
+
+//
+// Construct the Core module.
+
+module.exports = function(config) {
+
+
+ // High level method to call API
+ function api(method, path, params, callback) {
+
+ if(!callback || typeof(callback) !== 'function') {
+ throw new Error('Callback not provided on API call');
+ }
+
+ if (process.env.DEBUG) console.log('OAuth2 Node Request');
+ var url = config.client.site + path;
+
+ call(method, url, params, function(error, response, body) {
+ data(error, response, body, callback);
+ });
+ }
+
+
+ // Make the HTTP request
+ function call(method, url, params, callback) {
+
+ var options = { uri: url, method: method }
+ if (config.client) options.auth = { 'user': config.client.id, 'pass': config.client.secret }
+
+ if (isEmpty(params)) params = null;
+ method == 'GET' ? options.qs = params : options.json = params
+
+ if (process.env.DEBUG) console.log('Simple OAuth2: Making the HTTP request', options)
+ request(options, callback)
+ }
+
+
+ // Extract the data from the request response
+ function data(error, response, body, callback) {
+
+ if (error) throw new Error('Simple OAuth2: something went worng during the request');
+ if (process.env.DEBUG) console.log('Simple OAuth2: checking response body', body);
+
+ try { body = JSON.parse(body); }
+ catch(e) { body = errorResponse(response); }
+
+ if (response.statusCode >= 400) return callback(new HTTPError(body), null)
+
+ callback(error, body);
+ }
+
+
+ // 500 Error response
+ function errorResponse(response) {
+ return { "status": response.statusCode,
+ "request": response.request.uri.href,
+ "error": {
+ "code": "notifications.service.not_working",
+ "description": "Lelylan is not working correctly (we are investigating)."
+ }
+ }
+ }
+
+
+ // Personalized errror
+ function HTTPError(message) {
+ Error.call(this);
+ Error.captureStackTrace(this, this.constructor);
+ this.name = this.constructor.name;
+ this.message = message;
+ } HTTPError.prototype.__proto__ = Error.prototype;
+
+
+ function isEmpty(ob){
+ for(var i in ob){ return false;}
+ return true;
+ }
+
+
+ return {
+ 'call': call,
+ 'data': data,
+ 'api': api,
+ 'HTTPError': HTTPError
+ }
+};
View
43 lib/simple-oauth2.js
@@ -0,0 +1,43 @@
+//
+// A NodeJS module for interfacing with OAuth2. It accepts
+// an object with the following valid params.
+//
+// * `client.id` - Required registered Client ID.
+// * `client.secret` - Required registered Client secret.
+// * `client.site` - Required registered Client site.
+// * `authorizationPath` - Authorization path for the OAuth2 server.
+// Simple Oauth2 uses `/oauth/authorization` as default
+// * `tokenPath` - Access token path for the OAuth2 server.
+// Simple Oauth2 uses `/oauth/token` as default.
+//
+var appConfig = require('./config');
+
+
+// Configuration merge
+function mergeDefaults(o1, o2) {
+ for (var p in o2) {
+ try { if (typeof o2[p] == 'object') { o1[p] = mergeDefaults(o1[p], o2[p]); } else if (typeof o1[p] == 'undefined') { o1[p] = o2[p]; } }
+ catch(e) { o1[p] = o2[p]; }
+ }
+ return o1;
+}
+
+
+// Export the client we'll use to make requests
+module.exports = function(config) {
+
+ // Base configuration
+ function configure(config) {
+ config = config || {};
+ mergeDefaults(config, appConfig);
+
+ return config;
+ }
+
+ config = configure(config);
+ var core = require('./core')(config);
+
+ return {
+ 'AuthCode': require('./client/auth-code')(config),
+ }
+};
View
30 package.json
@@ -0,0 +1,30 @@
+{
+ "name": "simple-oauth2",
+ "version": "0.1.0",
+ "description": "Node.js client for OAuth2",
+ "author": "Andrea Reginato <andrea.reginato@gmail.com>",
+ "homepage": "http://github.com/andreareginato/simple-oauth2",
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/andreareginato/simple-oauth2"
+ },
+ "engine": {
+ "node": "~0.8.x"
+ },
+ "dependencies": {
+ "request": "~2.12.0",
+ "querystring": "~0.1.0"
+ },
+ "devDependencies": {
+ "should": "~1.2.1",
+ "mocha": "~1.8.1",
+ "nock": "~0.14.3",
+ "docco": "~0.4.0"
+ },
+ "main": ".",
+ "licenses": [
+ {
+ "type": "Copyright (c) 2013 Lelylan"
+ }
+ ]
+}
View
45 test/auth-code.js
@@ -0,0 +1,45 @@
+var credentials = { client: { id: 'client-id', secret: 'client-secret', site: 'https://example.org' } },
+ OAuth2 = require('./../lib/simple-oauth2.js')(credentials),
+ qs = require('querystring'),
+ nock = require('nock');
+
+var request, result, error;
+
+describe.only('OAuth2.AuthCode',function() {
+
+ describe('#authorizeURL', function(){
+
+ beforeEach(function(done) {
+ var params = { 'redirect_uri': 'http://localhost:3000/callback', 'scope': 'user', 'state': '02afe928b' };
+ result = OAuth2.AuthCode.authorizeURL(params);
+ done();
+ })
+
+ it('returns the authorization URI', function() {
+ var expected = 'https://example.org/oauth/authorization?redirect_uri=' + encodeURIComponent('http://localhost:3000/callback') + '&scope=user&state=02afe928b&response_type=code&client_id=client-id';
+ result.should.eql(expected);
+ })
+ });
+
+ //describe('#getToken',function() {
+
+ //beforeEach(function(done) {
+ //request = nock('http://api.lelylan.com').get('/devices/1').replyWithFile(200, __dirname + '/fixtures/device.json');
+ //done();
+ //})
+
+ //beforeEach(function(done) {
+ //Lelylan.Device.find('1', function(e, r) {
+ //error = e; response = r; done();
+ //})
+ //})
+
+ //it('makes the HTTP request', function() {
+ //request.isDone();
+ //});
+
+ //it('return a json array',function() {
+ //response.should.be.a('object');
+ //});
+ //});
+});
View
96 test/errors.js
@@ -0,0 +1,96 @@
+//var Lelylan = require('./../lib/lelylan-node.js')({ 'token': '5f7fb8f11b8499b' });
+//var nock = require('nock');
+//var request, response, error;
+
+//describe('Lelylan Error',function() {
+
+ //describe('with status code 500',function() {
+
+ //beforeEach(function(done) {
+ //request = nock('http://api.lelylan.com').get('/devices/1').reply(500, 'Internal Server Error');
+ //done();
+ //})
+
+ //beforeEach(function(done) {
+ //Lelylan.Device.find('1', function(e, r) {
+ //error = e; response = r; done();
+ //})
+ //})
+
+ //it('makes the HTTP request', function() {
+ //request.isDone();
+ //});
+
+ //it('return a json array',function() {
+ //error.should.be.a('object');
+ //});
+ //});
+
+ //describe('with status code 401',function() {
+
+ //beforeEach(function(done) {
+ //request = nock('http://api.lelylan.com').get('/devices/1').replyWithFile(401, __dirname + '/fixtures/errors/401.json');
+ //done();
+ //})
+
+ //beforeEach(function(done) {
+ //Lelylan.Device.find('1', function(e, r) {
+ //error = e; response = r; done();
+ //})
+ //})
+
+ //it('makes the HTTP request', function() {
+ //request.isDone();
+ //});
+
+ //it('return a json array',function() {
+ //error.should.be.a('object');
+ //});
+ //});
+
+ //describe('with status code 404',function() {
+
+ //beforeEach(function(done) {
+ //request = nock('http://api.lelylan.com').get('/devices/1').replyWithFile(404, __dirname + '/fixtures/errors/404.json');
+ //done();
+ //})
+
+ //beforeEach(function(done) {
+ //Lelylan.Device.find('1', function(e, r) {
+ //error = e; response = r; done();
+ //})
+ //})
+
+ //it('makes the HTTP request', function() {
+ //request.isDone();
+ //});
+
+ //it('return a json array',function() {
+ //error.should.be.a('object');
+ //});
+ //});
+
+ //describe('with status code 422',function() {
+
+ //beforeEach(function(done) {
+ //request = nock('http://api.lelylan.com').get('/devices/1').replyWithFile(422, __dirname + '/fixtures/errors/422.json');
+ //done();
+ //})
+
+ //beforeEach(function(done) {
+ //Lelylan.Device.find('1', function(e, r) {
+ //error = e; response = r; done();
+ //})
+ //})
+
+ //it('makes the HTTP request', function() {
+ //request.isDone();
+ //});
+
+ //it('return a json array',function() {
+ //error.should.be.a('object');
+ //});
+ //});
+//})
+
+
View
24 test/fixtures/device.json
@@ -0,0 +1,24 @@
+{
+ "uri": "http://api.lelylan.com/devices/5042344b95fc441000000001",
+ "id": "5042344b95fc441000000001",
+ "name": "Closet dimmer",
+ "type": { "uri": "http://api.lelylan.com/types/5042310470eda60a0000000d" },
+ "physical": { "uri": "http://arduino.house.com/5042205c70eda61" },
+ "properties": [{
+ "uri": "http://api.lelylan.com/properties/50420c7e267ff51000000001",
+ "id": "50420c7e267ff51000000001",
+ "value": "on",
+ "expected": "on",
+ "pending": false,
+ "suggested": {"on": "On", "off": "Off"}
+ }, {
+ "uri": "http://api.lelylan.com/properties/50420c3a267ff51300000001",
+ "id": "50420c3a267ff51300000001",
+ "value": "100",
+ "expected": "100",
+ "pending": false,
+ "suggested": {"0": "Min", "25": "Low", "50": "Half", "75": "High", "100": "Max"}
+ }],
+ "created_at": "2012-09-01T16:00:32Z",
+ "updated_at": "2012-09-01T16:00:32Z"
+}
View
24 test/fixtures/devices.json
@@ -0,0 +1,24 @@
+[{
+ "uri": "http://api.lelylan.com/devices/5042344b95fc441000000001",
+ "id": "5042344b95fc441000000001",
+ "name": "Closet dimmer",
+ "type": { "uri": "http://api.lelylan.com/types/5042310470eda60a0000000d" },
+ "physical": { "uri": "http://arduino.house.com/5042205c70eda61" },
+ "properties": [{
+ "uri": "http://api.lelylan.com/properties/50420c7e267ff51000000001",
+ "id": "50420c7e267ff51000000001",
+ "value": "on",
+ "expected": "on",
+ "pending": false,
+ "suggested": {"on": "On", "off": "Off"}
+ }, {
+ "uri": "http://api.lelylan.com/properties/50420c3a267ff51300000001",
+ "id": "50420c3a267ff51300000001",
+ "value": "100",
+ "expected": "100",
+ "pending": false,
+ "suggested": {"0": "Min", "25": "Low", "50": "Half", "75": "High", "100": "Max"}
+ }],
+ "created_at": "2012-09-01T16:00:32Z",
+ "updated_at": "2012-09-01T16:00:32Z"
+}]
View
9 test/fixtures/errors/401.json
@@ -0,0 +1,9 @@
+{
+ "status": 401,
+ "method": "POST",
+ "request": "http://api.lelylan.com/devices/4f4bb686d033a957c1000251",
+ "error": {
+ "code": "notifications.access.not_authorized",
+ "description": "Token not valid"
+ }
+}
View
10 test/fixtures/errors/404.json
@@ -0,0 +1,10 @@
+{
+ "status": 404,
+ "method": "GET",
+ "request": "http://api.lelylan.com/devices/4f4ba959d033a95549000261",
+ "error": {
+ "code": "notifications.resource.not_found",
+ "description": "Resource not found",
+ "uri": "http://api.lelylan.com/devices/4f4ba959d033a95549000261"
+ }
+}
View
10 test/fixtures/errors/422.json
@@ -0,0 +1,10 @@
+{
+ "status": 422,
+ "method": "POST",
+ "request": "http://api.lelylan.com/devices",
+ "error": {
+ "code": "notifications.resource.not_valid",
+ "description": "Name can't be blank.",
+ "body": { "name": "" },
+ }
+}
View
5 test/mocha.opts
@@ -0,0 +1,5 @@
+--require should
+--reporter dot
+--ui bdd
+--slow 20
+--growl
Please sign in to comment.
Something went wrong with that request. Please try again.