This commit is contained in:
Ritchie 2013-07-24 11:03:04 -07:00
commit 5172d879a1
15 changed files with 377 additions and 31 deletions

View File

@ -26,7 +26,7 @@ _TODO_
### App ### App
Create A Loopbackapplication. Create a Loopback application.
var loopback = require('loopback'); var loopback = require('loopback');
var app = loopback(); var app = loopback();

View File

@ -36,7 +36,7 @@ Application.create(data, function(err, data) {
}); });
Application.register('MyApp', 'My first mobile application', 'rfeng', function (err, result) { Application.register('rfeng', 'MyApp', {description: 'My first mobile application'}, function (err, result) {
console.log(result.toObject()); console.log(result.toObject());
result.resetKeys(function (err, result) { result.resetKeys(function (err, result) {

View File

@ -5,7 +5,7 @@
var DataSource = require('loopback-data').DataSource var DataSource = require('loopback-data').DataSource
, ModelBuilder = require('loopback-data').ModelBuilder , ModelBuilder = require('loopback-data').ModelBuilder
, assert = require('assert') , assert = require('assert')
, RemoteObjects = require('sl-remoting'); , RemoteObjects = require('strong-remoting');
/** /**
* Export the app prototype. * Export the app prototype.

View File

@ -35,7 +35,7 @@ function Connector(options) {
inherits(Connector, EventEmitter); inherits(Connector, EventEmitter);
/*! /*!
* Create an adapter instance from a JugglingDB adapter. * Create an connector instance from a JugglingDB adapter.
*/ */
Connector._createJDBAdapter = function (jdbModule) { Connector._createJDBAdapter = function (jdbModule) {
@ -49,6 +49,6 @@ Connector._createJDBAdapter = function (jdbModule) {
* Add default crud operations from a JugglingDB adapter. * Add default crud operations from a JugglingDB adapter.
*/ */
Connector.prototype._addCrudOperationsFromJDBAdapter = function (adapter) { Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) {
} }

View File

@ -23,7 +23,7 @@ var Connector = require('./base-connector')
*/ */
function Memory() { function Memory() {
// TODO implement entire memory adapter // TODO implement entire memory connector
} }
/** /**

View File

@ -3,7 +3,7 @@
*/ */
var loopback = require('../loopback'); var loopback = require('../loopback');
var RemoteObjects = require('sl-remoting'); var RemoteObjects = require('strong-remoting');
/** /**
* Export the middleware. * Export the middleware.

125
lib/models/README.md Normal file
View File

@ -0,0 +1,125 @@
# Application
Application model captures the metadata for a loopback application.
## Each application has the following basic properties:
* id: Automatically generated id
* name: Name of the application (required)
* description: Description of the application (optional)
* icon: URL of the icon
* status: Status of the application, such as production/sandbox/disabled
* created: Timestamp of the record being created
* modified: Timestamp of the record being modified
## An application has the following properties linking to users:
* owner: The user id of the developer who registers the application
* collaborators: A array of users ids who have permissions to work on this app
## oAuth 2.0 settings
* url: The application url
* callbackUrls: An array of preregistered callback urls for oAuth 2.0
* permissions: An array of oAuth 2.0 scopes that can be requested by the application
## Security keys
The following keys are automatically generated by the application creation process. They can be reset upon request.
* clientKey: Secret for mobile clients
* javaScriptKey: Secret for JavaScript clients
* restApiKey: Secret for REST APIs
* windowsKey: Secret for Windows applications
* masterKey: Secret for REST APIS. It bypasses model level permissions
## Push notification settings
The application can be configured to support multiple methods of push notifications.
* pushSettings
{
pushSettings: [
{ "platform": "apns",
"apns": {
"pushOptions": {
"gateway": "gateway.sandbox.push.apple.com",
"cert": "credentials/apns_cert_dev.pem",
"key": "credentials/apns_key_dev.pem"
},
"feedbackOptions": {
"gateway": "feedback.sandbox.push.apple.com",
"cert": "credentials/apns_cert_dev.pem",
"key": "credentials/apns_key_dev.pem",
"batchFeedback": true,
"interval": 300
}
}}
]}
## Authentication schemes
* authenticationEnabled
* anonymousAllowed
* authenticationSchemes
### Authentication scheme settings
* scheme: Name of the authentication scheme, such as local, facebook, google, twitter, linkedin, github
* credential: Scheme-specific credentials
## APIs for Application model
In addition to the CRUD methods, the Application model also has the following apis:
### Register a new application
You can register a new application by providing the owner user id, applicaiton name, and other properties in the options object.
Application.register('rfeng', 'MyApp1', {description: 'My first loopback application'}, function (err, result) {
var app = result;
...
});
### Reset keys
You can reset keys for a given application by id.
Application.resetKeys(appId, function (err, result) {
var app = result;
...
});
### Authenticate by appId and key
You can authenticate an application by id and one of the keys. If successful, it calls back with the key name in the result argument. Otherwise, the keyName is null.
Application.authenticate(appId, clientKey, function (err, keyName) {
assert.equal(keyName, 'clientKey');
...
});
# Installation
Installation captures the installation of the application on devices.
Each record of installation has the following properties:
* id: Generated id that uniquely identifies the installation
* appId: Application id
* appVersion: Application version
* userId: The current user id that logs into the application
* deviceToken: Device token
* deviceType: Device type, such as apns
* subscriptions: An Array of tags that represents subscriptions of notifications
* status: Status of the application, production/sandbox/disabled
* created: Timestamp of the recored being created
* modified: Timestamp of the recored being modified

View File

@ -20,19 +20,32 @@ Factors to be authorized against:
Class level permissions, for example, Album Class level permissions, for example, Album
* model name: Album * model name: Album
* methods * methods
// blog posts
allow: ['owner', 'admin'] to: '*' // allow owner's of posts and admins to do anything
allow: '*' to: ['find', 'read'] // allow everyone to read and find
// comments
allow '*' to: ['find', 'read'] // read aka findById
allow 'user' to: ['create']
allow ['owner', 'admin'] to: '*'
// users only section URL/Route level permissions
allow: '*' to: ['find', 'read', 'create'] * url pattern
allow: 'owner' to: ['*.destroy', '*.save'] * application id
* ip addresses
* http headers
// scopes Map to oAuth 2.0 scopes
// URL level permissions
*/ */
var ACLSchema = {
model: String, // The model name
properties: [String], // A list of property names
methods: [String], // A list of methods
roles: [String], // A list of roles
permission: {type: String, enum: ['Allow', 'Deny']}, // Allow/Deny
status: String, // Enabled/disabled
created: Date,
modified: Date
}
// readAccess, writeAccess --> public, userId, role
module.exports = function(dataSource) {
dataSource = dataSource || new require('loopback-data').ModelBuilder();
var ACL = dataSource.define('ACL', ACLSchema);
return ACL;
}

View File

@ -1,3 +1,5 @@
var assert = require('assert');
// Authentication schemes // Authentication schemes
var AuthenticationSchemeSchema = { var AuthenticationSchemeSchema = {
scheme: String, // local, facebook, google, twitter, linkedin, github scheme: String, // local, facebook, google, twitter, linkedin, github
@ -33,7 +35,7 @@ var PushNotificationSettingSchema = {
var ApplicationSchema = { var ApplicationSchema = {
// Basic information // Basic information
id: {type: String, required: true}, // The id id: {type: String, required: true, generated: true, id: true}, // The id
name: {type: String, required: true}, // The name name: {type: String, required: true}, // The name
description: String, // The description description: String, // The description
icon: String, // The icon image url icon: String, // The icon image url
@ -79,9 +81,10 @@ var ApplicationSchema = {
var crypto = require('crypto'); var crypto = require('crypto');
function generateKey(hmacKey, algorithm) { function generateKey(hmacKey, algorithm, encoding) {
hmacKey = hmacKey || 'loopback'; hmacKey = hmacKey || 'loopback';
algorithm = algorithm || 'sha256'; algorithm = algorithm || 'sha256';
encoding = encoding || 'base64';
var hmac = crypto.createHmac(algorithm, hmacKey); var hmac = crypto.createHmac(algorithm, hmacKey);
var buf = crypto.randomBytes(64); var buf = crypto.randomBytes(64);
hmac.update(buf); hmac.update(buf);
@ -102,7 +105,8 @@ module.exports = function (dataSource) {
// Application.hasMany(AuthenticationScheme, {as: 'authenticationSchemes', foreignKey: 'appId'}); // Application.hasMany(AuthenticationScheme, {as: 'authenticationSchemes', foreignKey: 'appId'});
// Application.hasMany(PushNotificationSetting, {as: 'pushNotificationSettings', foreignKey: 'appId'}); // Application.hasMany(PushNotificationSetting, {as: 'pushNotificationSettings', foreignKey: 'appId'});
Application.afterInitialize = function () { Application.beforeCreate = function (next) {
// console.trace();
var app = this; var app = this;
// use data argument to update object // use data argument to update object
app.created = app.modified = new Date(); app.created = app.modified = new Date();
@ -112,22 +116,84 @@ module.exports = function (dataSource) {
app.restApiKey = generateKey('restApi'); app.restApiKey = generateKey('restApi');
app.windowsKey = generateKey('windows'); app.windowsKey = generateKey('windows');
app.masterKey = generateKey('master'); app.masterKey = generateKey('master');
next();
}; };
// Register a new application /**
Application.register = function (name, description, owner, cb) { * Register a new application
Application.create({name: name, description: description, owner: owner}, cb); * @param owner Owner's user id
* @param name Name of the application
* @param options Other options
* @param cb Callback function
*/
Application.register = function (owner, name, options, cb) {
assert(owner, 'owner is required');
assert(name, 'name is required');
if(typeof options === 'function' && !cb) {
cb = options;
options = {};
}
var props = {owner: owner, name: name};
for(var p in options) {
if(!(p in props)) {
props[p] = options[p];
}
}
Application.create(props, cb);
} }
/**
* Reset keys for the application instance
* @param cb
*/
Application.prototype.resetKeys = function(cb) { Application.prototype.resetKeys = function(cb) {
this.clientKey = generateKey('client'); this.clientKey = generateKey('client');
this.javaScriptKey = generateKey('javaScript'); this.javaScriptKey = generateKey('javaScript');
this.restApiKey = generateKey('restApi'); this.restApiKey = generateKey('restApi');
this.windowsKey = generateKey('windows'); this.windowsKey = generateKey('windows');
this.masterKey = generateKey('master'); this.masterKey = generateKey('master');
this.modified = new Date();
this.save(cb); this.save(cb);
} }
/**
* Reset keys for a given application by the appId
* @param appId
* @param cb
*/
Application.resetKeys = function(appId, cb) {
Application.findById(appId, function(err, app) {
if(err) {
cb && cb(err, app);
return;
}
app.resetKeys(cb);
});
}
/**
*
* @param appId
* @param key
* @param cb
*/
Application.authenticate = function(appId, key, cb) {
Application.findById(appId, function(err, app) {
if(err || !app) {
cb && cb(err, null);
return;
}
var matched = null;
['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function(k) {
if(app[k] === key) {
matched = k;
}
});
cb && cb(null, matched);
});
}
return Application; return Application;
} }

View File

@ -1,6 +1,13 @@
exports.Model = require('./model');
exports.Email = require('./email');
exports.User = require('./user');
exports.Session = require('./session');
exports.Application = require('./application'); exports.Application = require('./application');
exports.ACL = require('./acl'); exports.ACL = require('./acl');
exports.Role = require('./role'); exports.Role = require('./role');
exports.Installation = require('./installation'); exports.Installation = require('./installation');

View File

@ -8,8 +8,8 @@ var InstallationSchema = {
appId: String, // Application id appId: String, // Application id
appVersion: String, // Application version appVersion: String, // Application version
userId: String, // User id userId: String, // User id
deviceToken: String, deviceToken: String, // Device token
deviceType: String, deviceType: String, // Device type, such as apns
subscriptions: [String], subscriptions: [String],
status: {type: String, default: 'active'}, // Status of the application, production/sandbox/disabled status: {type: String, default: 'active'}, // Status of the application, production/sandbox/disabled

View File

@ -9,7 +9,7 @@
"debug": "latest", "debug": "latest",
"express": "~3.1.1", "express": "~3.1.1",
"loopback-data": "git+ssh://git@github.com:strongloop/loopback-data.git", "loopback-data": "git+ssh://git@github.com:strongloop/loopback-data.git",
"sl-remoting": "git+ssh://git@github.com:strongloop/sl-remoting.git", "strong-remoting": "git+ssh://git@github.com:strongloop/strong-remoting.git",
"inflection": "~1.2.5", "inflection": "~1.2.5",
"bcrypt": "~0.7.6", "bcrypt": "~0.7.6",
"passport": "~0.1.17", "passport": "~0.1.17",

View File

@ -4,7 +4,7 @@ describe('loopback', function() {
var dataSource = loopback.createDataSource({ var dataSource = loopback.createDataSource({
connector: loopback.Memory connector: loopback.Memory
}); });
assert(dataSource.connector()); assert(dataSource.connector);
}); });
}); });
@ -31,4 +31,4 @@ describe('loopback', function() {
assert.equal(Product.stats.shared, true); assert.equal(Product.stats.shared, true);
}); });
}); });
}); });

View File

@ -0,0 +1,121 @@
var models = require('../lib/models');
var loopback = require(('../'));
var assert = require('assert');
var dataSource = loopback.createDataSource('db', {connector: loopback.Memory});
var Application = models.Application(dataSource);
describe('Application', function () {
var registeredApp = null;
it('Create a new application', function (done) {
Application.create({owner: 'rfeng', name: 'MyApp1', description: 'My first mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp1');
assert.equal(app.description, 'My first mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
done(err, result);
});
});
it('Register a new application', function (done) {
Application.register('rfeng', 'MyApp2', {description: 'My second mobile application'}, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
});
});
it('Reset keys', function (done) {
Application.resetKeys(registeredApp.id, function (err, result) {
var app = result;
assert.equal(app.owner, 'rfeng');
assert.equal(app.name, 'MyApp2');
assert.equal(app.description, 'My second mobile application');
assert(app.clientKey);
assert(app.javaScriptKey);
assert(app.restApiKey);
assert(app.windowsKey);
assert(app.masterKey);
assert(app.clientKey !== registeredApp.clientKey);
assert(app.javaScriptKey !== registeredApp.javaScriptKey);
assert(app.restApiKey !== registeredApp.restApiKey);
assert(app.windowsKey !== registeredApp.windowsKey);
assert(app.masterKey !== registeredApp.masterKey);
assert(app.created);
assert(app.modified);
registeredApp = app;
done(err, result);
});
});
it('Authenticate with application id & clientKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.clientKey, function (err, result) {
assert.equal(result, 'clientKey');
done(err, result);
});
});
it('Authenticate with application id & javaScriptKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.javaScriptKey, function (err, result) {
assert.equal(result, 'javaScriptKey');
done(err, result);
});
});
it('Authenticate with application id & restApiKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.restApiKey, function (err, result) {
assert.equal(result, 'restApiKey');
done(err, result);
});
});
it('Authenticate with application id & masterKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.masterKey, function (err, result) {
assert.equal(result, 'masterKey');
done(err, result);
});
});
it('Authenticate with application id & windowsKey', function (done) {
Application.authenticate(registeredApp.id, registeredApp.windowsKey, function (err, result) {
assert.equal(result, 'windowsKey');
done(err, result);
});
});
it('Fail to authenticate with application id & invalid key', function (done) {
Application.authenticate(registeredApp.id, 'invalid-key', function (err, result) {
assert(!result);
done(err, result);
});
});
});

View File

@ -192,7 +192,21 @@ describe('Model', function() {
}); });
}); });
describe('Model.destroyAll(callback)', function() { describe('Model.deleteById([callback])', function () {
it("Delete a model instance from the attached data source", function (done) {
User.create({first: 'joe', last: 'bob'}, function (err, user) {
User.deleteById(user.id, function (err) {
User.findById(user.id, function (err, notFound) {
assert(!err);
assert.equal(notFound, null);
done();
});
});
});
});
});
describe('Model.destroyAll(callback)', function() {
it("Delete all Model instances from data source", function(done) { it("Delete all Model instances from data source", function(done) {
(new TaskEmitter()) (new TaskEmitter())
.task(User, 'create', {first: 'jill'}) .task(User, 'create', {first: 'jill'})