DEV Community 👩‍💻👨‍💻

Rachel
Rachel

Posted on

Part 2: User Roles and Management - FeathersJS

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);
  });
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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'),
        },
      })
    )
  );
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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/"
}
Enter fullscreen mode Exit fullscreen mode

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(),
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

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),
]),
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await