I would like to add to YeeHaw1234's answer. I plan to use Mixins the way he describes, but I needed more fields than just User ID to filter the data. I have 3 other fields that I wanted to add to the Access token so I could enforce data rules at the lowest possible level.
I wanted to add some fields to the session, but couldn't figure out how in Loopback. I looked at express-session and cookie-express, but the problem was that I did not want to rewrite the Loopback login and Login seemed like the place where the session fields should be set.
My solution was to create a Custom User and Custom Access Token and add the fields I needed. I then used an operation hook (before save) to insert my new fields before a new Access Token was written.
Now every time someone logs in, I get my extra fields. Feel free to let me know if there is an easier way to add fields to a session. I plan to add an update Access Token so that if the user's permissions change while they are logged in that they will see those changes in the session.
Here is some of the code.
/common/models/mr-access-token.js
var app = require('../../server/server');
module.exports = function(MrAccessToken) {
MrAccessToken.observe('before save', function addUserData(ctx, next) {
const MrUser = app.models.MrUser;
if (ctx.instance) {
MrUser.findById(ctx.instance.userId)
.then(result => {
ctx.instance.setAttribute("role");
ctx.instance.setAttribute("teamId");
ctx.instance.setAttribute("leagueId");
ctx.instance.setAttribute("schoolId");
ctx.instance.role = result.role;
ctx.instance.teamId = result.teamId;
ctx.instance.leagueId = result.leagueId;
ctx.instance.schoolId = result.schoolId;
next();
})
.catch(err => {
console.log('Yikes!');
})
} else {
MrUser.findById(ctx.instance.userId)
.then(result => {
ctx.data.setAttribute("role");
ctx.data.setAttribute("teamId");
ctx.data.setAttribute("leagueId");
ctx.data.setAttribute("schoolId");
ctx.data.role = result.role;
ctx.data.teamId = result.teamId;
ctx.data.leagueId = result.leagueId;
ctx.data.schoolId = result.schoolId;
next();
})
.catch(err => {
console.log('Yikes!');
})
}
})
};
This took me a long time to debug. Here was a few hurdles I had. I initially thought it needed to be in /server/boot, but I wasn't seeing the code triggered on saves. When I moved it to /common/models it started firing. Trying to figure out how to reference a 2nd model from within the observer wasn't in the docs. The var app = ... was in another SO answer. The last big problem was I had the next() outside the async findById so the instance was being returned unchanged and then the async code would modify the value.
/common/models/mr-user.js
{
"name": "MrUser",
"base": "User",
"options": {
"idInjection": false,
"mysql": {
"schema": "matrally",
"table": "MrUser"
}
},
"properties": {
"role": {
"type": "String",
"enum": ["TEAM-OWNER",
"TEAM-ADMIN",
"TEAM-MEMBER",
"SCHOOL-OWNER",
"SCHOOL-ADMIN",
"SCHOOL-MEMBER",
"LEAGUE-OWNER",
"LEAGUE-ADMIN",
"LEAGUE-MEMBER",
"NONE"],
"default": "NONE"
}
},
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "MrAccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
},
"league": {
"model": "League",
"type": "belongsTo"
},
"school": {
"model": "School",
"type": "belongsTo"
},
"team": {
"model": "Team",
"type": "belongsTo"
}
}
}
/common/models/mr-user.js
{
"name": "MrAccessToken",
"base": "AccessToken",
"options": {
"idInjection": false,
"mysql": {
"schema": "matrally",
"table": "MrAccessToken"
}
},
"properties": {
"role": {
"type": "String"
}
},
"relations": {
"mrUser": {
"model": "MrUser",
"type": "belongsTo"
},
"league": {
"model": "League",
"type": "belongsTo"
},
"school": {
"model": "School",
"type": "belongsTo"
},
"team": {
"model": "Team",
"type": "belongsTo"
}
}
}
/server/boot/mrUserRemoteMethods.js
var senderAddress = "curtis@abcxyz.com"; //Replace this address with your actual address
var config = require('../../server/config.json');
var path = require('path');
module.exports = function(app) {
const MrUser = app.models.MrUser;
//send verification email after registration
MrUser.afterRemote('create', function(context, user, next) {
var options = {
type: 'email',
to: user.email,
from: senderAddress,
subject: 'Thanks for registering.',
template: path.resolve(__dirname, '../../server/views/verify.ejs'),
redirect: '/verified',
user: user
};
user.verify(options, function(err, response) {
if (err) {
MrUser.deleteById(user.id);
return next(err);
}
context.res.render('response', {
title: 'Signed up successfully',
content: 'Please check your email and click on the verification link ' +
'before logging in.',
redirectTo: '/',
redirectToLinkText: 'Log in'
});
});
});
// Method to render
MrUser.afterRemote('prototype.verify', function(context, user, next) {
context.res.render('response', {
title: 'A Link to reverify your identity has been sent '+
'to your email successfully',
content: 'Please check your email and click on the verification link '+
'before logging in',
redirectTo: '/',
redirectToLinkText: 'Log in'
});
});
//send password reset link when requested
MrUser.on('resetPasswordRequest', function(info) {
var url = 'http://' + config.host + ':' + config.port + '/reset-password';
var html = 'Click <a href="' + url + '?access_token=' +
info.accessToken.id + '">here</a> to reset your password';
MrUser.app.models.Email.send({
to: info.email,
from: senderAddress,
subject: 'Password reset',
html: html
}, function(err) {
if (err) return console.log('> error sending password reset email');
console.log('> sending password reset email to:', info.email);
});
});
//render UI page after password change
MrUser.afterRemote('changePassword', function(context, user, next) {
context.res.render('response', {
title: 'Password changed successfully',
content: 'Please login again with new password',
redirectTo: '/',
redirectToLinkText: 'Log in'
});
});
//render UI page after password reset
MrUser.afterRemote('setPassword', function(context, user, next) {
context.res.render('response', {
title: 'Password reset success',
content: 'Your password has been reset successfully',
redirectTo: '/',
redirectToLinkText: 'Log in'
});
});
};
This is straight from the examples, but it wasn't clear that it should be registered in /boot. I couldn't get my custom user to send emails until I moved it from /common/models to /server/boot.