Skip to content

Cantango with devise accounts

kristianmandrup edited this page Oct 19, 2011 · 5 revisions

This page will demonstrate a scenario, where each Devise model is an account. Then we will have a User model which can be a member of one or more of these accounts. For each account, the user can have an individual password and some of these accounts might have stricter policies, such as password strength and the whole sign-up workflow might also vary between accounts.

Having separate account models makes sense in more complex scenarios fx when your application can be seen as having several sub-applications. A simple example of this is an app, where there is a public app for members (normal users) and an admin app (fx using active_admin) for Admin users.

In this example we will work with the Devise models as distinct accounts and separate user models in a 1-Many relationship. Each user can have multiple accounts and each account is for one particular user. Since a user has multiple accounts, each account can have different credentials and security policies.

Generating a Devise UserAccount model:

$ rails generate devise UserAccount

class UserAccount < ActiveRecord::Base
  tango_user_account

  devise :database_authenticatable, :registerable, :confirmable, :recoverable, # ...

  belongs_to :user
end

We register this class as a user account class using the tango_user_accountmacro. CanTango uses this registration to generate the Account API. The devise macro defines the authentication and security profile of this account.

A common scenario is then to create an Admin account class like this:

Generating a Devise AdminAccount model:

$ rails g devise AdminAccount

class AdminAccount < ActiveRecord::Base
  tango_user_account

  devise :database_authenticatable, :registerable, :confirmable, :recoverable, # ...

  belongs_to :user
end

You could also have the AdminAccount class inherit from UserAccount and override where necessary.

The AdminAccount class can be specialized to have its own devise strategies, validation and other logic. One example is to require a stricter password enforcement strategy and another sign-up procedure (security strategy).

Now let's generate a user model which we hook up with the above accounts:

$ rails g model User first_name:string last_name:string

class User < ActiveRecord::Base
  tango_user

  attr_accessor :account

  has_one :admin_account
  has_one :user_account
end

Alternatively we could set up the User <-> Account relationship using a polymorphic relation.

class Account < ActiveRecord::Base
  belongs_to :user
end

class UserAccount < Account
  tango_user_account
end

class AdminAccount < Account
  tango_user_account
end
class User < ActiveRecord::Base
  tango_user

  attr_accessor :account

  has_many :accounts, :polymorphic => true
end

Note that the explicit naming of the account classes with the 'Account' postfix, results in all the Devise generated routes and methods not follow the usual naming "convention". You could streamline this naming to follow the conventions more closely by having the accounts named simply User and Admin and then rename the "real" user class as fx Person, Profile or Character depending on what fits with your domain model. Note that the relationships between the User-Account models must have the names #account and #user. If you choose this approach you must remember, that accessing current_user and similar methods returns the user "account" and not the "real user" (or person).

The :account accessor on a user should be assigned whichever account is currently active (fx for a given request).

The CanTango User API will have the user_can? method use the current_user method. Since we have set up Devise with accounts and not users, the current_user method will not be available in this configuration. We thus have to roll our own:

class ApplicationController
  def current_user
    session[:user]
  end
end

Then we should assign this session slot when we log in to either of the accounts. Alternatively we could use the method any_account something like this:

  def current_user
    session[:user] ||= any_account.user
  end

Note: You might want to only store the id of the object in the session and fetch it using this id. This will get any logged-in account for the session and then get the user of that account. We also need to take care of resetting session[:user] when logging out of an account.

Now make your App aware of your devise models in your routes. Here and example using the explicit account naming strategy:

  # routes.rb
  devise_for :user_accounts, :admin_accounts

This will generate distinct Devise route sets for both UserAccount and AdminAccount user types. The following Devise methods will be made available in views and controllers

  • current_user_account - the logged in UserAccount (if any)
  • current_admin_account - the logged in AdminAccount user (if any)
  • user_account_signed_in? - is there a logged in UserAccount for the session ?
  • admin_account_signed_in? - is there a logged in AdminAccount for the session ?

We hope this discussion will get you going... There are many ways to "skin a cat".