Merge branch 'release/2.7.0' into production
This commit is contained in:
commit
c0b1225a1c
|
@ -5,6 +5,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"id": true
|
"id": true
|
||||||
},
|
},
|
||||||
|
"realm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true
|
"required": true
|
||||||
|
|
|
@ -353,7 +353,11 @@ module.exports = function(Role) {
|
||||||
context.principals.forEach(function(p) {
|
context.principals.forEach(function(p) {
|
||||||
// Check against the role mappings
|
// Check against the role mappings
|
||||||
var principalType = p.type || undefined;
|
var principalType = p.type || undefined;
|
||||||
var principalId = p.id || undefined;
|
var principalId = p.id == null ? undefined : p.id;
|
||||||
|
|
||||||
|
if(typeof principalId !== 'string' && principalId != null) {
|
||||||
|
principalId = principalId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// Add the role itself
|
// Add the role itself
|
||||||
if (principalType === RoleMapping.ROLE && principalId) {
|
if (principalType === RoleMapping.ROLE && principalId) {
|
||||||
|
|
|
@ -57,6 +57,57 @@ User.prototype.createAccessToken = function(ttl, cb) {
|
||||||
}, cb);
|
}, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function splitPrincipal(name, realmDelimiter) {
|
||||||
|
var parts = [null, name];
|
||||||
|
if(!realmDelimiter) {
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
var index = name.indexOf(realmDelimiter);
|
||||||
|
if (index !== -1) {
|
||||||
|
parts[0] = name.substring(0, index);
|
||||||
|
parts[1] = name.substring(index + realmDelimiter.length);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the credentials
|
||||||
|
* @param {Object} credentials The credential object
|
||||||
|
* @param {Boolean} realmRequired
|
||||||
|
* @param {String} realmDelimiter The realm delimiter, if not set, no realm is needed
|
||||||
|
* @returns {Object} The normalized credential object
|
||||||
|
*/
|
||||||
|
User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) {
|
||||||
|
var query = {};
|
||||||
|
credentials = credentials || {};
|
||||||
|
if(!realmRequired) {
|
||||||
|
if (credentials.email) {
|
||||||
|
query.email = credentials.email;
|
||||||
|
} else if (credentials.username) {
|
||||||
|
query.username = credentials.username;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (credentials.realm) {
|
||||||
|
query.realm = credentials.realm;
|
||||||
|
}
|
||||||
|
var parts;
|
||||||
|
if (credentials.email) {
|
||||||
|
parts = splitPrincipal(credentials.email, realmDelimiter);
|
||||||
|
query.email = parts[1];
|
||||||
|
if (parts[0]) {
|
||||||
|
query.realm = parts[0];
|
||||||
|
}
|
||||||
|
} else if (credentials.username) {
|
||||||
|
parts = splitPrincipal(credentials.username, realmDelimiter);
|
||||||
|
query.username = parts[1];
|
||||||
|
if (parts[0]) {
|
||||||
|
query.realm = parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login a user by with the given `credentials`.
|
* Login a user by with the given `credentials`.
|
||||||
*
|
*
|
||||||
|
@ -88,16 +139,25 @@ User.login = function(credentials, include, fn) {
|
||||||
include = include.toLowerCase();
|
include = include.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realmDelimiter;
|
||||||
|
// Check if realm is required
|
||||||
|
var realmRequired = !!(self.settings.realmRequired ||
|
||||||
|
self.settings.realmDelimiter);
|
||||||
|
if (realmRequired) {
|
||||||
|
realmDelimiter = self.settings.realmDelimiter;
|
||||||
|
}
|
||||||
|
var query = self.normalizeCredentials(credentials, realmRequired,
|
||||||
|
realmDelimiter);
|
||||||
|
|
||||||
var query = {};
|
if(realmRequired && !query.realm) {
|
||||||
if (credentials.email) {
|
var err1 = new Error('realm is required');
|
||||||
query.email = credentials.email;
|
err1.statusCode = 400;
|
||||||
} else if (credentials.username) {
|
return fn(err1);
|
||||||
query.username = credentials.username;
|
}
|
||||||
} else {
|
if (!query.email && !query.username) {
|
||||||
var err = new Error('username or email is required');
|
var err2 = new Error('username or email is required');
|
||||||
err.statusCode = 400;
|
err2.statusCode = 400;
|
||||||
return fn(err);
|
return fn(err2);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.findOne({where: query}, function(err, user) {
|
self.findOne({where: query}, function(err, user) {
|
||||||
|
@ -283,9 +343,10 @@ User.prototype.verify = function(options, fn) {
|
||||||
from: options.from,
|
from: options.from,
|
||||||
subject: options.subject || 'Thanks for Registering',
|
subject: options.subject || 'Thanks for Registering',
|
||||||
text: options.text,
|
text: options.text,
|
||||||
html: template(options)
|
html: template(options),
|
||||||
}, function(err, email) {
|
headers: options.headers || {}
|
||||||
if (err) {
|
}, function (err, email) {
|
||||||
|
if(err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else {
|
} else {
|
||||||
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
fn(null, {email: email, token: user.verificationToken, uid: user.id});
|
||||||
|
@ -488,9 +549,14 @@ User.setup = function() {
|
||||||
// email validation regex
|
// email validation regex
|
||||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
|
||||||
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'});
|
||||||
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
|
||||||
|
// FIXME: We need to add support for uniqueness of composite keys in juggler
|
||||||
|
if (!(UserModel.settings.realmRequired || UserModel.settings.realmDelimiter)) {
|
||||||
|
UserModel.validatesUniquenessOf('email', {message: 'Email already exists'});
|
||||||
|
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
||||||
|
}
|
||||||
|
|
||||||
return UserModel;
|
return UserModel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -509,6 +509,10 @@ PersistedModel.setupRemoting = function() {
|
||||||
rest: {
|
rest: {
|
||||||
// After hook to map exists to 200/404 for HEAD
|
// After hook to map exists to 200/404 for HEAD
|
||||||
after: function(ctx, cb) {
|
after: function(ctx, cb) {
|
||||||
|
if (ctx.req.method === 'GET') {
|
||||||
|
// For GET, return {exists: true|false} as is
|
||||||
|
return cb();
|
||||||
|
}
|
||||||
if(!ctx.result.exists) {
|
if(!ctx.result.exists) {
|
||||||
var modelName = ctx.method.sharedClass.name;
|
var modelName = ctx.method.sharedClass.name;
|
||||||
var id = ctx.getArgByName('id');
|
var id = ctx.getArgByName('id');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "loopback",
|
"name": "loopback",
|
||||||
"version": "2.6.0",
|
"version": "2.7.0",
|
||||||
"description": "LoopBack: Open Source Framework for Node.js",
|
"description": "LoopBack: Open Source Framework for Node.js",
|
||||||
"homepage": "http://loopback.io",
|
"homepage": "http://loopback.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -29,6 +29,18 @@ describe('loopback.rest', function() {
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report 200 for GET /:id/exists not found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
request(app).get('/mymodels/1/exists')
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.body).to.eql({exists: false});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should report 200 for GET /:id found', function(done) {
|
it('should report 200 for GET /:id found', function(done) {
|
||||||
app.model(MyModel);
|
app.model(MyModel);
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
|
@ -49,6 +61,20 @@ describe('loopback.rest', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report 200 for GET /:id/exists found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
MyModel.create({name: 'm2'}, function(err, inst) {
|
||||||
|
request(app).get('/mymodels/' + inst.id + '/exists')
|
||||||
|
.expect(200)
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.body).to.eql({exists: true});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('includes loopback.token when necessary', function(done) {
|
it('includes loopback.token when necessary', function(done) {
|
||||||
givenUserModelWithAuth();
|
givenUserModelWithAuth();
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
|
|
|
@ -293,6 +293,12 @@ describe('User', function(){
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function assertGoodToken(accessToken) {
|
||||||
|
assert(accessToken.userId);
|
||||||
|
assert(accessToken.id);
|
||||||
|
assert.equal(accessToken.id.length, 64);
|
||||||
|
}
|
||||||
|
|
||||||
describe('User.login requiring email verification', function() {
|
describe('User.login requiring email verification', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
User.settings.emailVerificationRequired = true;
|
User.settings.emailVerificationRequired = true;
|
||||||
|
@ -311,9 +317,7 @@ describe('User', function(){
|
||||||
|
|
||||||
it('Login a user by with email verification', function(done) {
|
it('Login a user by with email verification', function(done) {
|
||||||
User.login(validCredentialsEmailVerified, function (err, accessToken) {
|
User.login(validCredentialsEmailVerified, function (err, accessToken) {
|
||||||
assert(accessToken.userId);
|
assertGoodToken(accessToken);
|
||||||
assert(accessToken.id);
|
|
||||||
assert.equal(accessToken.id.length, 64);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -328,9 +332,7 @@ describe('User', function(){
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
var accessToken = res.body;
|
var accessToken = res.body;
|
||||||
|
|
||||||
assert(accessToken.userId);
|
assertGoodToken(accessToken);
|
||||||
assert(accessToken.id);
|
|
||||||
assert.equal(accessToken.id.length, 64);
|
|
||||||
assert(accessToken.user === undefined);
|
assert(accessToken.user === undefined);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@ -349,6 +351,166 @@ describe('User', function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('User.login requiring realm', function() {
|
||||||
|
var User, AccessToken;
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
User = loopback.User.extend('RealmUser', {},
|
||||||
|
{realmRequired: true, realmDelimiter: ':'});
|
||||||
|
AccessToken = loopback.AccessToken.extend('RealmAccessToken');
|
||||||
|
|
||||||
|
loopback.autoAttach();
|
||||||
|
|
||||||
|
// Update the AccessToken relation to use the subclass of User
|
||||||
|
AccessToken.belongsTo(User);
|
||||||
|
User.hasMany(AccessToken);
|
||||||
|
|
||||||
|
// allow many User.afterRemote's to be called
|
||||||
|
User.setMaxListeners(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
var realm1User = {
|
||||||
|
realm: 'realm1',
|
||||||
|
username: 'foo100',
|
||||||
|
email: 'foo100@bar.com',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var realm2User = {
|
||||||
|
realm: 'realm2',
|
||||||
|
username: 'foo100',
|
||||||
|
email: 'foo100@bar.com',
|
||||||
|
password: 'pass200'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialWithoutRealm = {
|
||||||
|
username: 'foo100',
|
||||||
|
email: 'foo100@bar.com',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialWithBadPass = {
|
||||||
|
realm: 'realm1',
|
||||||
|
username: 'foo100',
|
||||||
|
email: 'foo100@bar.com',
|
||||||
|
password: 'pass001'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialWithBadRealm = {
|
||||||
|
realm: 'realm3',
|
||||||
|
username: 'foo100',
|
||||||
|
email: 'foo100@bar.com',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialWithRealm = {
|
||||||
|
realm: 'realm1',
|
||||||
|
username: 'foo100',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialRealmInUsername = {
|
||||||
|
username: 'realm1:foo100',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var credentialRealmInEmail = {
|
||||||
|
email: 'realm1:foo100@bar.com',
|
||||||
|
password: 'pass100'
|
||||||
|
};
|
||||||
|
|
||||||
|
var user1;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
User.create(realm1User, function(err, u) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
user1 = u;
|
||||||
|
User.create(realm2User, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function(done) {
|
||||||
|
User.deleteAll({realm: 'realm1'}, function(err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
User.deleteAll({realm: 'realm2'}, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a user by without realm', function(done) {
|
||||||
|
User.login(credentialWithoutRealm, function(err, accessToken) {
|
||||||
|
assert(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a user by with bad realm', function(done) {
|
||||||
|
User.login(credentialWithBadRealm, function(err, accessToken) {
|
||||||
|
assert(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a user by with bad pass', function(done) {
|
||||||
|
User.login(credentialWithBadPass, function(err, accessToken) {
|
||||||
|
assert(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs in a user by with realm', function(done) {
|
||||||
|
User.login(credentialWithRealm, function(err, accessToken) {
|
||||||
|
assertGoodToken(accessToken);
|
||||||
|
assert.equal(accessToken.userId, user1.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs in a user by with realm in username', function(done) {
|
||||||
|
User.login(credentialRealmInUsername, function(err, accessToken) {
|
||||||
|
assertGoodToken(accessToken);
|
||||||
|
assert.equal(accessToken.userId, user1.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs in a user by with realm in email', function(done) {
|
||||||
|
User.login(credentialRealmInEmail, function(err, accessToken) {
|
||||||
|
assertGoodToken(accessToken);
|
||||||
|
assert.equal(accessToken.userId, user1.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User.login with realmRequired but no realmDelimiter', function() {
|
||||||
|
before(function() {
|
||||||
|
User.settings.realmDelimiter = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
User.settings.realmDelimiter = ':';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs in a user by with realm', function(done) {
|
||||||
|
User.login(credentialWithRealm, function(err, accessToken) {
|
||||||
|
assertGoodToken(accessToken);
|
||||||
|
assert.equal(accessToken.userId, user1.id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a user by with realm in email if realmDelimiter is not set',
|
||||||
|
function(done) {
|
||||||
|
User.login(credentialRealmInEmail, function(err, accessToken) {
|
||||||
|
assert(err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('User.logout', function() {
|
describe('User.logout', function() {
|
||||||
it('Logout a user by providing the current accessToken id (using node)', function(done) {
|
it('Logout a user by providing the current accessToken id (using node)', function(done) {
|
||||||
|
@ -487,6 +649,38 @@ describe('User', function(){
|
||||||
if(err) return done(err);
|
if(err) return done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Verify a user\'s email address with custom header', function(done) {
|
||||||
|
User.afterRemote('create', function(ctx, user, next) {
|
||||||
|
assert(user, 'afterRemote should include result');
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
type: 'email',
|
||||||
|
to: user.email,
|
||||||
|
from: 'noreply@myapp.org',
|
||||||
|
redirect: '/',
|
||||||
|
protocol: ctx.req.protocol,
|
||||||
|
host: ctx.req.get('host'),
|
||||||
|
headers: {'message-id':'custom-header-value'}
|
||||||
|
};
|
||||||
|
|
||||||
|
user.verify(options, function (err, result) {
|
||||||
|
assert(result.email);
|
||||||
|
assert.equal(result.email.messageId, 'custom-header-value');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app)
|
||||||
|
.post('/users')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.send({email: 'bar@bat.com', password: 'bar'})
|
||||||
|
.end(function(err, res){
|
||||||
|
if(err) return done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('User.confirm(options, fn)', function () {
|
describe('User.confirm(options, fn)', function () {
|
||||||
|
|
Loading…
Reference in New Issue