Using Refinery with Rails 3.1 and Devise

This guide covers adding functionality provided by Refinery CMS to an existing application, while retaining the existing application’s control over configuration and use of the Devise authentication gem. After reading it, you should be familiar with:

  • Getting Refinery CMS + a Rails 3.1.x app
  • Adding the authentication (and authorization) rules from your app to Refinery’s back end administration content.
  • Ensuring your existing app’s tests still runs
Edit this guide on Github

1 Guide Assumptions

This guide assumes you are starting with a working Ruby On Rails v3.1.x application that is using Devise for authentication. It does not assume you are using any particular authorization gem like CanCan. The instructions have been tested with Devise v1.4.0.

This guide takes an approach of leaving the Refinery Gems unmodified to make it easier to upgrade them without re-merging in your necessary modifications. You’d just have to keep an eye on the User model.

1.1 Assumptions About The Existing Application

Refinery CMS edge assumes you want a content management system to control your site, which makes it very easy to build a Refinery CMS based application from scratch, and modify that. Fortunately, it is also flexible enough to take on a smaller role to add functionality to your existing application. This guides focuses on those situations where there are large areas of your existing application that Refinery CMS should not be involved with at all. For example, you want to add a blog using refinery’s blog extension, to your existing site, but don’t want to otherwise alter your existing, perhaps complex, site.

Since this guide can’t anticipate the unique integration issues with your situation, we’ll create a minimal Rails + Devise application to reference. We’ll assume that the existing application already handles authentication and authorization in a way that must be maintained to avoid breaking existing functionality, and that there are models, controllers and views that you don’t want Refinery CMS to be involved with. Specifically, we’ll assume that the ExistingApp will handle administration of the User table records through a non-refinery interface (perhaps the console, perhaps some ExistingApp controller).

In creating the existing application, we’ll make the following convenient, but not required asumptions:

  • you’re using ruby version manager. The particular ruby version and gemset used here can be whatever you need and are just placeholders.
  • you’re using older versions of some gems than refinery also expects, to demonstrate one way to deal with that situation.
  • you’re using git for source code control.

Attaching Refinery CMS to an existing Rails application has further details about integrating with an existing application in general.

2 Creating the existing Rails + Devise application

These commands should speak for themselves if you previously created your own rails + devise app to integrate with.

gem install rails # ensure it is greater than 3.1.3 and less than 4.0.0
rails new ExistingApp
cd ExistingApp
Gemfile
# ...

  gem 'devise', '~> 1.4.0' # Not recommending a specific version, just saying perhaps your app used an older version than refinery requires, and you aren't quite ready to upgrade, just to demonstrate...

  # ...
bundle
rails generate devise:install
rails generate devise User # Devise might be using another model name in your existing application
bundle exec rake db:migrate

Next we’ll add a static, unrestricted home page, the notice and alert flashes to the application default layout, and a restricted_content controller and view.

We’ll also add a simple stylesheet, which in a real existing app, would be what we would like Refinery to integrate with on the front end, rather than refinery overriding it.

To demonstrate another wrinkle you might discover, let’s customize our use of Devise a tiny bit, by overriding the session controller to handle a before_filter to prevent logging in when the database is undergoing maintenance.

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Existing Application</title>
    <%= stylesheet_link_tag    "application" %>
    <%= javascript_include_tag "application" %>
    <%= csrf_meta_tags %>
  </head>
  <body>
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
    <%= yield %>
  </body>
</html>
app/controllers/static_controller.rb
class StaticController < ApplicationController
  # using default layouts and views, so don't need to define methods here.
end
app/views/static/home.html.erb
<h1>Home is where you hang your hat.</h1>
app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  before_filter :block_login_during_maintenance
  def block_login_during_maintenance
    return unless DB_UNDER_MAINTENANCE
    redirect_to db_maintenance_message_path
    return false
  end
end
config/initializers/db_maintenance.rb
DB_UNDER_MAINTENANCE = false
app/views/static/db_maintenance.html.erb
<h1>Sorry, the database is undergoing maintenance. Areas requiring login are currently unavailable.</h1>
app/controllers/restricted_content_controller.rb
class RestrictedContentController < ApplicationController
  before_filter :authenticate_user!
end
app/views/restricted_content/vault.html.erb
<h1>You are inside the vault.</h1>
config/routes.rb
ExistingApp::Routes.draw do
    # ...

    get "vault", :to => "restricted_content#vault"
    get "static/db_maintenance", :as => "db_maintenance_message"
    root :to => "static#home"

    # ...
  end
app/assets/stylesheets/front_end_styles.css

/* not pretty, but you’ll know it when you see it*/ body { background-color: #1100aa } p, a, h1, h2, h3, h4 { color: white }

rm public/index.html
rails generate devise:views
mkdir app/views/users
mkdir app/views/users/sessions
cp app/views/devise/sessions/new.html.erb app/views/users/sessions/.
app/views/users/sessions/new.html.erb
...
Our custom new session view
# Leave the rest unchanged
...
rails console
User.create :email=>"my_email@my_company.com", :password=>"my_pa$$word"

Go to http://localhost:3000 and you should see the home page. Go to http://localhost:3000/vault and you should be able to sign in and then see the vault page.

3 Integrating Refinery CMS into ExistingApp

Now that we have a baseline example working ExistingApp using Devise, it’s time to integrate Refinery, using our existing front end styles, existing user models, and whatever specialized authentication and authorization code we may already have in place.

Gemfile
# ...

  gem 'devise' # temporarily commented out,'~> 1.4.0'

  gem 'refinerycms-dashboard', '~> 2.1.0'
  gem 'refinerycms-images', '~> 2.1.0'
  gem 'refinerycms-pages', '~> 2.1.0'
  gem 'refinerycms-resources', '~> 2.1.0'

  gem 'refinerycms-testing', '~> 2.1.0', :group => :test

  # ...
bundle # or bundle update if you have version conflicts
rails generate refinery:cms # Say no if asked to override your devise.rb file.

Now we have to undo some of what the refinery generator did, to keep control of the front end, and let refinery still handle the admin backend, as well as not squashing our existing user table.

mv app/views/layouts/application.html.erb.backup app/views/layouts/application.html.erb
mkdir -p app/assets/stylesheets/hide
mv app/assets/stylesheets/*.css app/assets/stylesheets/hide/
mv app/assets/stylesheets/hide/front_end_styles.css app/assets/stylesheets/

Now we need to setup up:

  • A Role model
  • An additional model to get our many to many association working: RolesUsers
  • A model used to list all Refinery plugins belonging to someone: UserPlugin

These are created by default when you add the refinerycms-authentication gem so we won’t reinvent the wheel and use the existing code taken from github. Be aware that your existing models could do the trick (except for UserPlugin), as long as you implement some methods but we’ll see that later.

Beware of raw code: models are usually namespaced. Ex: replace ::Refinery::Role with ::Role or simply Role

With inspiration provided by the example at https://github.com/refinery/refinerycms/blob/master/authentication/db/migrate/20100913234705_create_refinerycms_authentication_schema.rb we will generate the following models:

rails g model RolesUsers user_id:integer role_id:integer # add the index directly within the migration file
rails g model Role title:string
rails g model UserPlugin user_id:integer name:string position:integer

Now we have to fill in our models. Once again, we won’t re-invent the wheel so we’ll pick the code from here: https://github.com/refinery/refinerycms/blob/2.1.0/authentication/app/models/refinery/

app/model/role.rb
class Role < Refinery::Core::BaseModel
  attr_accessible :title

  # TODO: This works around a bug in rails habtm with namespaces.
  has_and_belongs_to_many :users, :join_table => ::RolesUsers.table_name

  before_validation :camelize_title
  validates :title, :uniqueness => true

  def camelize_title(role_title = self.title)
    self.title = role_title.to_s.camelize
  end

  def self.[](title)
    find_or_create_by_title(title.to_s.camelize)
  end
end
app/model/roles_users.rb
class RolesUsers < Refinery::Core::BaseModel
  belongs_to :role
  belongs_to :user
end
app/model/user_plugin.rb
class UserPlugin < Refinery::Core::BaseModel
  belongs_to :user
  attr_accessible :user_id, :name, :position
end
app/model/user.rb
class User < Refinery::Core::BaseModel
  # Your stuff
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  attr_accessible :email, :password, :password_confirmation, :remember_me

  # End of your stuff

  has_and_belongs_to_many :roles, :join_table => ::RolesUsers.table_name
  has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy

  def plugins=(plugin_names)
    if persisted? # don't add plugins when the user_id is nil.
      UserPlugin.delete_all(:user_id => id)

      plugin_names.each_with_index do |plugin_name, index|
        plugins.create(:name => plugin_name, :position => index) if plugin_name.is_a?(String)
      end
    end
  end

  def authorized_plugins
    plugins.collect(&:name) | ::Refinery::Plugins.always_allowed.names
  end

  def add_role(title)
    if title.is_a? ::Role
      raise ArgumentException, "Role should be the title of the role not a role object."
    end

    roles << ::Role[title] unless has_role?(title)
  end

  def has_role?(title)
    if title.is_a? ::Role
      raise ArgumentException, "Role should be the title of the role not a role object."
    end

    roles.any? { |r| r.title == title.to_s.camelize}
  end

  def can_delete?(user_to_delete = self)
    user_to_delete.persisted? &&
      !user_to_delete.has_role?(:superuser) &&
      ::Refinery::Role[:refinery].users.any? &&
      id != user_to_delete.id
  end

  def can_edit?(user_to_edit = self)
    user_to_edit.persisted? && (user_to_edit == self || self.has_role?(:superuser))
  end
end
bundle exec rake db:migrate

Now we need to add authentication goodness manually, it will just consist in telling refinery to use your devise helpers.

We’ll add this in your lib directory:

mkdir lib/refinery
touch lib/refinery/refinery_patch.rb
touch lib/refinery/restrict_refinery_to_refinery_users.rb

Then open the files and put the content from this gist inside: https://gist.github.com/1272720

Next, we’ll have to load the content of these files in your app.

config/application.rb
module ExistingApp
  class Application < Rails::Application
    #...
    # Load files from the lib directory, including subfolders.
    config.autoload_paths += Dir["#{config.root}/lib/**/"]
    config.before_initialize do
      require 'refinery_patch'
      require 'restrict_refinery_to_refinery_users'
    end

    extend Refinery::Engine
    after_inclusion do
      [::ApplicationController, ::ApplicationHelper, ::Refinery::AdminController].each do |c|
        c.send :include, ::RefineryPatch
      end

      ::Refinery::AdminController.send :include, ::RestrictRefineryToRefineryUsers
      ::Refinery::AdminController.send :before_filter, :restrict_refinery_to_refinery_users
    end
  end
end

Next we need to give refinerycms-core/lib/controllers/application_controller.rb a valid Refinery user so it won’t take over with a new user form.

rails console
first_user = User.first # created above to test devise
first_user.roles << Role.create(:title=>"Superuser")
first_user.roles << Role.create(:title=>"Refinery")
first_user.save
exit

We need to also avoid showing the new user form in test mode or any integration tests in ExistingApp will suddenly stop working.

For branch 2-0-stable the relevant method is refinery_user_required? in app/controllers/application_controller

class ApplicationController < ActionController::Base
  #...

  def refinery_user_required?
    false
  end

  # ...
end

For branch master the relevant method is require_refinery_users! in app/controllers/refinery/admin/base_controller

Refinery::Admin::BaseController.class_eval do
  def require_refinery_users!
    false
  end
end

Ok. We should be ready to give it a spin.

If you try to access http://localhost:3000/, you should get first an error undefined method 'refinery_user?'. Just reload the page, it should now be fine.

If you try to access http://localhost:3000/refinery (Refinery’s administrative backend), boom again: “undefined method `destroy_refinery_user_session_path’”.

Indeed, we have to correct one view to set the proper url helper.

bundle exec rake refinery:override view=refinery/_site_bar

Now the partial has been moved to your app, change destroy_refinery_user_session_path, with destroy_user_session_path. Also important, add :method => :delete if it’s not in the link_to helper.

Retry http://localhost:3000/refinery, it should be fine now.

Now if you want to be able to edit the User model from refinery, then you should make sure the User table matches the Refinery::User table exactly. In most cases with devise, it is as simple as adding the username column to the users table and making that attribute accessible in the model. Next you can set up the following initializer:

config/initializers/refinery_class_substitutions.rb
class Refinery::User < User; end
class Refinery::Role < Role; end

Now you can restart your server, and manage users instead of refinery_users.

Last advice: in your routes, map your root with refinery/pages#home if you want Refinery to handle your homepage.

ExistingApp::Routes.draw do
  # ...

  root :to => 'refinery/pages#home'

  # ...
end