Created
July 28, 2015 02:51
-
-
Save spencermefford/bc73812f216e0e254ad1 to your computer and use it in GitHub Desktop.
An alternative to extending Loopback's built in models. In our application, we wanted to create a custom role called "ecm-administrator" that would have the ability to create and manage users.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = function (app) { | |
var _ = require('lodash'); | |
var User = app.models.User; | |
var Role = app.models.Role; | |
var RoleMapping = app.models.RoleMapping; | |
var ACL = app.models.ACL; | |
/* | |
* Configure ACL's | |
*/ | |
ACL.create({ | |
model: 'User', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
ACL.create({ | |
model: 'Role', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
ACL.create({ | |
model: 'RoleMapping', | |
property: '*', | |
accessType: '*', | |
principalType: 'ROLE', | |
principalId: 'ecm-administrator', | |
permission: 'ALLOW' | |
}, function (err, acl) { // Create the acl | |
if (err) console.error(err); | |
}); | |
/* | |
* Add hooks | |
*/ | |
RoleMapping.observe('before save', function filterProperties(ctx, next) { | |
/* | |
* Since there is no built in method to add users to roles in Loopback via REST API, we have leveraged | |
* the hasManyThrough relationship to handle this. Unfortunately, the RoleMapping model has an extra | |
* field called principalType that a typical join table would not have. We have to manually set this. | |
*/ | |
if (_.isEmpty(ctx.instance.principalType)) { // If no principalType has been set... | |
ctx.instance.principalType = RoleMapping.USER; // Set it to USER since it's likely that the User REST API is creating this | |
} | |
if (!_.isEmpty(ctx.instance.userId)) { | |
ctx.instance.principalId = ctx.instance.userId; | |
ctx.instance.unsetAttribute('userId'); | |
} | |
next(); | |
}); | |
/* | |
* Configure relationships | |
*/ | |
RoleMapping.belongsTo(User); | |
RoleMapping.belongsTo(Role); | |
User.hasMany(Role, {through: RoleMapping, foreignKey: 'principalId'}); | |
User.hasMany(RoleMapping, {foreignKey: 'principalId'}); | |
Role.hasMany(User, {through: RoleMapping, foreignKey: 'roleId'}); | |
/* | |
* Add additional attributes to models. | |
*/ | |
Role.defineProperty('label', { type: 'string' }); // Add a role label that is user readable | |
User.defineProperty('firstName', { type: 'string' }); // Give the user a first name field | |
User.defineProperty('lastName', { type: 'string' }); // Give the user a last name field | |
/** | |
* Add a user to the given role. | |
* @param {string} userId | |
* @param {string} roleId | |
* @param {Function} cb | |
*/ | |
User.addRole = function(userId, roleId, cb) { | |
var error; | |
User.findOne({ where: { id: userId } }, function(err, user) { // Find the user... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(user)) { | |
Role.findOne({ where: { id: roleId } }, function(err, role) { // Find the role... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(role)) { | |
RoleMapping.findOne({ where: { principalId: userId, roleId: roleId } }, function(err, roleMapping) { // Find the role mapping... | |
if (err) cb(err); // Error | |
if (_.isEmpty(roleMapping)) { // Only create if one doesn't exist to avoid duplicates | |
role.principals.create({ | |
principalType: RoleMapping.USER, | |
principalId: user.id | |
}, function(err, principal) { | |
if (err) cb(err); // Error | |
cb(null, role); // Success, return role object | |
}); | |
} else { | |
cb(null, role); // Success, return role object | |
} | |
}); | |
} else { | |
error = new Error('Role.' + roleId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
} else { | |
error = new Error('User.' + userId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
}; | |
User.remoteMethod( | |
'addRole', | |
{ | |
accepts: [ | |
{arg: 'userId', type: 'string'}, | |
{arg: 'roleId', type: 'string'} | |
], | |
http: { | |
path: '/add-role', | |
verb: 'post' | |
}, | |
returns: {type: 'object', root: true} | |
} | |
); | |
/** | |
* Remove a user from the given role. | |
* @param {string} userId | |
* @param {string} roleId | |
* @param {Function} cb | |
*/ | |
User.removeRole = function(userId, roleId, cb) { | |
var error; | |
User.findOne({ where: { id: userId } }, function(err, user) { // Find the user... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(user)) { | |
Role.findOne({ where: { id: roleId } }, function(err, role) { // Find the role... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(role)) { | |
RoleMapping.findOne({ where: { principalId: userId, roleId: roleId } }, function(err, roleMapping) { // Find the role mapping... | |
if (err) cb(err); // Error | |
if (!_.isEmpty(roleMapping)) { | |
roleMapping.destroy(function(err) { | |
if (err) cb(err); // Error | |
cb(null, role); // Success, return role object | |
}); | |
} else { | |
cb(null, role); // Success, return role object | |
} | |
}); | |
} else { | |
error = new Error('Role.' + roleId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
} else { | |
error = new Error('User.' + userId + ' was not found.'); | |
error.http_code = 404; | |
cb(error); // Error | |
} | |
}); | |
}; | |
User.remoteMethod( | |
'removeRole', | |
{ | |
accepts: [ | |
{arg: 'userId', type: 'string'}, | |
{arg: 'roleId', type: 'string'} | |
], | |
http: { | |
path: '/remove-role', | |
verb: 'post' | |
} | |
} | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@spencermefford Thanks for this, nice work 👍
I've made a fork which has a few changes here: https://gist.github.com/leftclickben/aa3cf418312c0ffcc547
Most of my changes are purely code style, but there are two things worth pointing out:
if (err) cb(err);
you really want to doif (err) return cb(err);
(addreturn
) otherwise the method will continue and strange things may happen. For example, in yourUser.addRole
method, if theUser.find
comes back with an error, you will call the callback with that error, and then call it again with your own error further down. Adding thereturn
will prevent this, and can also help prevent nesting blocks too deeply.addRole
/removeRole
methods from static methods (User.addRole
) to instance methods (User.prototype.addRole
), and specifiedisStatic: false
in the API config. This way you don't need the wholeUser.find
part because the method is called on theUser
instance which is retrieved for you by the framework / router. That is,this
inside the method refers to the instance retrieved by the id in the URL.My version also differs in that I'm passing the role
name
in rather than theid
, because my clients only know about names and have no canonical source for the correctid
values for the roles.