-
Notifications
You must be signed in to change notification settings - Fork 45
Cantango with devise accounts
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_account
macro. 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".