Part 2: User Roles and Management - FeathersJS
Rachel
Posted on September 8, 2020
The Backend - FeathersJS
This article focuses on the backend, which leverages the FeathersJS framework and several complementary libraries in the FeatherJS Ecosystem: feathers-authentication-management and feathers-permissions.
Getting Started With FeatherJS
Getting started with FeathersJS is pretty easy. There's a CLI that generates an application based on several configurable options.
Note: I would advise against copying/pasting any code snippets and instead go directly to the repository to view the code. Due to the amount of code involved, I've omitted lines of code in this article for brevity. As this is not written as a tutorial, please view the repository if you want to create a similar project.
FeathersJS Overview
Feathers has a great getting started guide, so I'd highly recommend reviewing their guide for a more in-depth overview. I'll highlight a few features that are customized for this starter.
Configuration
With Feathers, configuration is fairly straightforward. It takes the NODE_ENV
environment variable to determine which configuration to use. For example, if NODE_ENV=prod
, then it will merge the default.json
with prod.json
configuration settings. We'll add some settings to the configuration file to ensure services have the necessary values to run properly.
On Login
I wanted to store a timestamp for when a user logs in, so I used the app.on('login'...) connection event.
app.on('login', (data) => {
data.user['lastLoggedIn'] = new Date();
app.service('users').patch(data.user._id, data.user);
});
Feathers Services
Feathers services can be generated using the command line generator with feathers generate service
. This will begin a prompt sequence that configures the service to your needs. Services in feathers consist of a class, hooks, and a service definition.
Feathers Mailer
To send emails, the server uses the feathers-mailer library, which is a wrapper for nodemailer. For this starter, I configured it for AWS SES, but you can use any supported transport. To configure for AWS, the following configuration keys will be needed from your AWS account:
{
"smtp_user": "aws_smtp_user",
"smtp_pw": "aws_smtp_pw",
"smtp_host": "aws_smtp_host"
}
You can add these to the ${env}.json
configuration file or default.json
configuration file.
I created a custom service using the feathers cli and configured it for the AWS SES Transport. The email service shows how this is set up.
module.exports = function (app) {
app.use(
'/email',
Mailer(
smtpTransport({
host: app.get('smtp_host'),
secure: true,
auth: {
user: app.get('smtp_user'),
pass: app.get('smtp_pw'),
},
})
)
);
};
Once the email service is configured, it can be used to verify emails on sign up with the feathers-authentication-management library.
Coming Soon: I'll be writing a separate article about creating beautiful templated emails to send out using this service.
Feathers Authentication Management
Feathers Authentication Management is a library that enables several useful features during the user signup process:
- email verification
- password reset
- update password
- update to new email with verification
To add it to the user workflow, I created an auth-management service.
const authManagement = require('feathers-authentication-management');
const hooks = require('./auth-management.hooks');
const notifier = require('./notifier');
module.exports = function (app) {
app.configure(authManagement(notifier(app)));
// Get our initialized service so that we can register hooks
const service = app.service('authManagement');
service.hooks(hooks);
};
The notifier processes the incoming request and handles the case accordingly based on the action
received from the request. The resendVerifySignup
case will resend the verification email to the user.
function sendEmail(email) {
return app
.service('email')
.create(email)
.catch((err) => {
console.log('Error sending email', err);
});
}
switch (type) {
case 'resendVerifySignup':
//sending the user the verification email
tokenLink = getLink('verify', user.verifyToken);
email = {
from: FROM_EMAIL,
to: user.email,
subject: 'Verify Email',
html: tokenLink,
};
return sendEmail(email);
}
To ensure this service has all the necessary information to generate the correct email, the following configuration keys were also added to the ${env}.json
file.
{
"from_email": "no-reply@test.com",
"client_url": "http://localhost:8080",
"api_url": "http://localhost:3030/"
}
Hooks are used to update the user record before and after various actions.
module.exports = {
before: {
create: [
// after user is created, add verification fields to user record
verifyHooks.addVerification(),
],
patch: [
authenticate('jwt'),
iff(
// if request is from external provider
isProvider('external'),
// do not allow the following fields to be updating
preventChanges(
true,
'email',
'isVerified',
'verifyToken',
'verifyShortToken',
'verifyExpires',
'verifyChanges',
'resetToken',
'resetShortToken',
'resetExpires'
),
),
],
// don't allow external requests to delete the user
remove: [disallow('external')],
},
after: {
all: [
// prevent leak of these user information fields
protect(
'password',
'verifyToken',
'updatedAt',
'createdAt',
'verifyShortToken',
'verifyExpires',
'resetToken',
'resetExpires',
'verifyChanges',
'__v'
),
],
create: [
// after a user is created, send the user an email to verify email
(context) => {
accountService(context.app).notifier(
'resendVerifySignup',
context.data
);
},
// remove the user verification fields before returning user as part of request
verifyHooks.removeVerification(),
],
},
};
After a user is created, the verification fields are added to the user (and later removed before being return as part of a request). For security purposes, fields also should not be directly updated by external requests. After a user is created, another hook sends the user a verification email before removing the verification fields from the user.
Feathers Permissions
Finally, the backend implements the role concept using feathers-permissions, using a manually created admin account. Accounts default to a 'guest' role defined in the mongoose model (covered in the next article).
The admin role can update users into other roles. This starter defines 4 roles: 'guest' (default role), 'user', 'admin', 'inactive'. If an admin deactivates a user, it will update their role to 'inactive'.
Hooks are used to control and limit access to specific admin functions, such as updating a user role.
iff(
checkPermissions({
roles: ['super_admin', 'admin'],
field: 'permissions',
error: false,
}),
validate.mongoose(adminUpdateSchema, joiOptions)
),
iff((context) => !context.params.permitted, [
// ensure user only updates their own record
setField({
from: 'params.user._id',
as: 'params.query._id',
}),
validate.mongoose(updateSchema, joiOptions),
]),
The hooks above check if the user is an admin, and if so, check the data against the approved admin schema (which allows more fields to be updated). If the user isn't an admin, make sure the user only updates their own record with the approved user schema.
The permissions can be refined even further. Check the documentation for details. I kept it simple for this starter.
Additional Resources
Special shoutout to these authors who wrote tutorials for setting up email verification.
Did I miss anything?
This wraps up the backend code. Let me know if you have any questions, comments or suggestions. In the next article, I'll be reviewing the MongoDB setup!
Posted on September 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.