Skip to content
This repository has been archived by the owner on Mar 8, 2018. It is now read-only.

Users, Roles & Groups

Reza Akhavan edited this page Aug 30, 2015 · 9 revisions

The goal of this page is to explain the connection between users, roles and groups. To best understand these concepts see these schema files from the project:

  • /schema/User.js
  • /schema/Account.js
  • /schema/Admin.js
  • /schema/AdminGroup.js

Users

The User schema was designed to facilitate the login system no matter what role is being played. After a user is authenticated you have access to the roles that user can play. User instances have the following methods:

  • User#canPlayRoleOf - out of the box, you'll find this method used in the /routes.js file to limit access to specific areas of the site.
  • User#defaultReturnUrl - though not often, users can play multiple roles. This method helps us determine a good place to send users when we don't know where else to send them (ex: after login with no returnUrl supplied).

The User schema has a field isActive which the login screen uses to validate credentials. It can be used as a simple way to prevent a user from logging in. It's not recommended to make it represent anything more than that.

Roles (Accounts, Admins & Custom)

There are certainly some ideas/projects that could be wildly successful and never use the Admin role in Drywall. You're not required to make use of it. However, once a system has users, we usually need a way to manage and support them.

There are some important differences between what Account and Admin roles were designed for. Think of Accounts as the "customers" of a system and think of Admins as the "owners" and/or "employees". Take a support ticket feature for example. Accounts would be the users that created support tickets, while Admins would be the users who could see all tickets, be assigned to them, reply to them and see reports about them.

Why are roles separate from Users? For sanity really. Did you realize that both Admins and Accounts are indeed Users? Both of them need to login, both of them can forget their passwords and have them reset. Storing user information on both the Account and Admin schemas would complicate the login logic, forcing us to overly abstract it or even duplicate it by creating two separate login screens.

A recent feature added to the project was the ability to validate Account email addresses by sending an email to the person who registered and having them click a link. This feature was only necessary for Accounts, as Admins wouldn't need this functionality since they can't register. So putting the "validation" logic on the Account made the most sense and keeps the functionality contained for what it's used for. Keep this in mind when you extend your system.

Can I have more than two roles? Yes.

Adding a New Role

Let's say that we wanted to create a new role in our system for our Affiliates. Of course this assumes that we want Affiliates to login and have a custom experience unlike Accounts or Admins.

Step 1

Create the Affiliate schema /schema/Affiliate.js

'use strict';

exports = module.exports = function(app, mongoose) {
  var affiliateSchema = new mongoose.Schema({
    user: {
      id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
      name: { type: String, default: '' }
    },
    company: { type: String, default: '' },
    phone: { type: String, default: '' },
    zip: { type: String, default: '' }
  });
  affiliateSchema.plugin(require('./plugins/pagedFind'));
  affiliateSchema.index({ user: 1 });
  affiliateSchema.set('autoIndex', (app.get('env') === 'development'));
  app.db.model('Affiliate', affiliateSchema);
};

Step 2

Add the new schema to /models.js

...
require('./schema/Affiliate')(app, mongoose);
...

Step 3

Extend the roles field in /schema/User.js

...
roles: {
  admin: { type: mongoose.Schema.Types.ObjectId, ref: 'Admin' },
  account: { type: mongoose.Schema.Types.ObjectId, ref: 'Account' },
  affiliate: { type: mongoose.Schema.Types.ObjectId, ref: 'Affiliate' }
},
...

Don't forget to extend the defaultReturnUrl method too

...
userSchema.methods.defaultReturnUrl = function() {
  var returnUrl = '/';
  if (this.canPlayRoleOf('account')) {
    returnUrl = '/account/';
  }
  
  if (this.canPlayRoleOf('affiliate')) {
    returnUrl = '/affiliate/';
  }
  
  if (this.canPlayRoleOf('admin')) {
    returnUrl = '/admin/';
  }
  
  return returnUrl;
};
...

And don't forget to extend the canPlayRoleOf method also

...
userSchema.methods.canPlayRoleOf = function(role) {
  if (role === "admin" && this.roles.admin) {
    return true;
  }

  if (role === "account" && this.roles.account) {
    return true;
  }

  if (role === "affiliate" && this.roles.affiliate) {
    return true;
  }

  return false;
};
...

Step 4

Update the passport.deserializeUser method in the /passport.js file to populate the affiliate role too.

passport.deserializeUser(function(id, done) {
  app.db.models.User.findOne({ _id: id }).populate('roles.admin').populate('roles.account').populate('roles.affiliate').exec(function(err, user) {
    if (user.roles && user.roles.admin) {
      user.roles.admin.populate("groups", function(err, admin) {
        done(err, user);
      });
    }
    else {
      done(err, user);
    }
  });
});

Step 5

Create an ensureAffiliate middleware function In /routes.js

...
function ensureAffiliate(req, res, next) {
  if (req.user.canPlayRoleOf('affiliate')) {
    return next();
  }
  res.redirect('/');
}
...

Use the new middleware function on the routes for affiliate views

...
//affiliate
app.all('/affiliate*', ensureAuthenticated);
app.all('/affiliate*', ensureAffiliate);
app.get('/affiliate/', require('./views/affiliate/index').init);
...

Further Steps

You'll probably want to create admin views to manage Affiliates like we have for Accounts. Simply reference the code in /public/views/admin/accounts/ and /views/admin/accounts/ and create what you need for affiliates.

If you want the global admin search to also include Affiliates you'll need to make sure you have a search field on the /schema/Affiliate.js file (like we do for Accounts) and then extend the /views/admin/search/index.js file to search Affiliates also.

Thinking About Registration

The registration feature that comes with Drywall won't work for our new Affiliate role. As it shouldn't. Affiliates, like Admins, should be unique enough roles that they deserve a unique creation flow. Remember that Admins can't register either. And that makes sense, we create other Admins in the backend, if we have permission to of course.

Roles -vs- Groups

Roles should be unique enough to deserve their own schema. For example, if I'm a company, I could use my Admin schema to represent my employees. Then, I could add fields to my Admin schema for things like hourly rate, home address and benefits (other roles wouldn't have that type of data). Then I could use AdminGroups like departments (ex: Sales, Support, Manager). Now I can create permissions tied to AdminGroups and change the user experience based on that. Simply adding and removing a user's group memberships automatically changes their access permissions. Pretty sweet!

Can I use groups with Accounts? Sure. Your project might benefit by extending Accounts with an AccountGroup schema (and permissions). For example, if I built a message board or wiki and I wanted some Accounts to moderate other Accounts or change site content... groups would be the perfect solution. It would be a huge overkill to create a new roll (and schema) for a Moderator, which is like an Account in every way, only differing by a permission.

Use the Force

I hope this was helpful. If you have questions or think this page should be expanded please contribute by opening an issue or updating this page.