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
Create A Loopbackapplication.
Create a Loopback application.
var loopback = require('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());
result.resetKeys(function (err, result) {

View File

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

View File

@ -35,7 +35,7 @@ function Connector(options) {
inherits(Connector, EventEmitter);
/*!
* Create an adapter instance from a JugglingDB adapter.
* Create an connector instance from a JugglingDB adapter.
*/
Connector._createJDBAdapter = function (jdbModule) {
@ -49,6 +49,6 @@ Connector._createJDBAdapter = function (jdbModule) {
* 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() {
// TODO implement entire memory adapter
// TODO implement entire memory connector
}
/**

View File

@ -3,7 +3,7 @@
*/
var loopback = require('../loopback');
var RemoteObjects = require('sl-remoting');
var RemoteObjects = require('strong-remoting');
/**
* 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
* model name: Album
* 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
allow: '*' to: ['find', 'read', 'create']
allow: 'owner' to: ['*.destroy', '*.save']
URL/Route level permissions
* url pattern
* 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
var AuthenticationSchemeSchema = {
scheme: String, // local, facebook, google, twitter, linkedin, github
@ -33,7 +35,7 @@ var PushNotificationSettingSchema = {
var ApplicationSchema = {
// 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
description: String, // The description
icon: String, // The icon image url
@ -79,9 +81,10 @@ var ApplicationSchema = {
var crypto = require('crypto');
function generateKey(hmacKey, algorithm) {
function generateKey(hmacKey, algorithm, encoding) {
hmacKey = hmacKey || 'loopback';
algorithm = algorithm || 'sha256';
encoding = encoding || 'base64';
var hmac = crypto.createHmac(algorithm, hmacKey);
var buf = crypto.randomBytes(64);
hmac.update(buf);
@ -102,7 +105,8 @@ module.exports = function (dataSource) {
// Application.hasMany(AuthenticationScheme, {as: 'authenticationSchemes', foreignKey: 'appId'});
// Application.hasMany(PushNotificationSetting, {as: 'pushNotificationSettings', foreignKey: 'appId'});
Application.afterInitialize = function () {
Application.beforeCreate = function (next) {
// console.trace();
var app = this;
// use data argument to update object
app.created = app.modified = new Date();
@ -112,22 +116,84 @@ module.exports = function (dataSource) {
app.restApiKey = generateKey('restApi');
app.windowsKey = generateKey('windows');
app.masterKey = generateKey('master');
next();
};
// Register a new application
Application.register = function (name, description, owner, cb) {
Application.create({name: name, description: description, owner: owner}, cb);
/**
* Register a new application
* @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) {
this.clientKey = generateKey('client');
this.javaScriptKey = generateKey('javaScript');
this.restApiKey = generateKey('restApi');
this.windowsKey = generateKey('windows');
this.masterKey = generateKey('master');
this.modified = new Date();
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;
}

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.ACL = require('./acl');
exports.Role = require('./role');
exports.Installation = require('./installation');

View File

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

View File

@ -9,7 +9,7 @@
"debug": "latest",
"express": "~3.1.1",
"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",
"bcrypt": "~0.7.6",
"passport": "~0.1.17",

View File

@ -4,7 +4,7 @@ describe('loopback', function() {
var dataSource = loopback.createDataSource({
connector: loopback.Memory
});
assert(dataSource.connector());
assert(dataSource.connector);
});
});
@ -31,4 +31,4 @@ describe('loopback', function() {
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) {
(new TaskEmitter())
.task(User, 'create', {first: 'jill'})