Using Refinery with Rails 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 to use an existing User model
  • Applying your site layout and css to the Refinery front end generated content
  • Ensuring your existing app’s tests still run
  • Adding the authentication (and authorization) rules from your app to Refinery’s back end administration content.
Edit this guide on Github

This guide is based on Refinery CMS 1.0.0 so some of the code shown here may not work in earlier versions of Refinery.

1 Guide Assumptions

This guide assumes you are starting with a working Ruby On Rails v3.0.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.1.8.

1.1 Different approaches to integration

You can exclude the Authentication Refinery Engine from your application by specifically including the other refinery gems, as shown in this gist. That approach requires you to manually create, at least, a Roles table that the authentication gem would normally handle. This approach is not explored further in this guide, but some of the steps here may still apply with the approach of excluding the authentication gem. (For example, where the other refinery gems make assumptions about the users table that may not match your ExistingApp’s user’s table.)

You can also create your own modified Refinery Gems.

This guides takes a third approach of leaving the Refinery Gems unmodified to make it easier to upgrade them without re-merging in your necessary modifications. Hopefully the approach used here will be robust enough to Refinery CMS upgrades and improvements to allow you to benefit from all the great refinerycms contributors without constantly modifying your code.

1.2 Assumptions About The Existing Application

Refinery CMS 1.0.0 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.

$ mkdir ExistingApp
$ cd ExistingApp
$ rvm use 1.8.7-p174 # for example, could be another ruby compatible with refinery
$ rvm gemset create 'existing'
$ echo "rvm use 1.8.7-p174@existing" > .rvmrc
$ cd . #to trigger rvm to approve and start using .rvmrc setting
$ git init #if you are using git for source code control. Git commit steps will be left out of this guide to keep it focused.
$ cd ..
$ rails -v #probably want to ensure > 3.0.0
$ #if the rails gem isn't there to get your app generated you might do the following two steps..
$ rvm use 1.8.7-p174@existing #if necessary
$ gem install rails #if necessary
$ rails new ExistingApp
$ cd ExistingApp
Gemfile
...
  gem 'devise', '=1.1.8' #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...
  gem 'jquery-rails'
...
$ bundle install
$ rails generate jquery:install --ui
$ rails generate devise:install
$ rails generate devise User #Devise might be using another model name in your existing app
$ rails generate migration add_member_name_to_users member_name:string #add a display name for the user, also to demonstrate what to do when refinery is expecting a different field name.
$ 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/controllers/static_controller.rb
class StaticController < ApplicationController
#using default layouts and views, so don't need to define methods here.
end
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>ExistingApp</title>
    <%= stylesheet_link_tag :all %>
    <%= javascript_include_tag :defaults %>
    <%= csrf_meta_tags %>
  </head>
  <body>
    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
  <%= yield %>

  </body>
</html>
config/routes.rb
...
  match "vault" =>"restricted_content#vault"
  get "static/db_maintenance", :as=>"db_maintenance_message"
  root :to => "static#home"
  ...

+app/views/static/home.html.erb

<h1>Home is where you hang your hat.</h1>
app/views/static/db_maintenance.html.erb
<h1>Sorry, the database is undergoing maintenance. Areas requiring login are currently unavailable.</h1>
rm public/index.html

+public/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
}
app/views/restricted_content/vault.html.erb
<h1>You are inside the vault.</h1>
app/controllers/restricted_content_controller.rb
class RestrictedContentController < ApplicationController
  before_filter :authenticate_user!
end

+app/controllers/users/sessions_controller.rb

class Users::SessionsController < Devise::SessionsController
  before_filter :block_login_during_maintenance
  def block_login_during_maintenance
   return if !DB_UNDER_MAINTENANCE
   redirect_to db_maintenance_message_path
   return false
  end
config/initializers/db_maintenance.rb
DB_UNDER_MAINTENANCE = false
$ 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 c
ruby> me = User.new(:email=>"my_email@my_company.com", :password=>"my_pa$$word")
...
ruby> me.save
true
ruby> exit
$ rails s

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.

$ git add -A
$ git commit

3 Integrating Refinery CMS into ExistingApp

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

Gemfile
...
  gem 'refinerycms', '1.0.4' #for example
  gem 'refinerycms-blog', '~>1.6.1'
...
$ bundle install
Bundler could not find compatible versions for gem "devise":
  In Gemfile:
    refinerycms (=1.0.4) depends on
      devise (~>1.3.0)

    devise (1.1.8)

In this case, devise is being managed by ExistingApp, so you may be able to use the previous version. (Or it may be time to upgrade ExistingApp to avoid any problems…) But if you really need to, you can override the versions of gems by modifying Gemfile.lock after the dependencies have been calculated. This is obviously messy, and probably not a situation you want to stay with for long…

Gemfile
...
  gem 'devise' #temporarily commented out,'=1.1.8'
...
$ bundle update devise
Gemfile.lock
...
  devise (1.1.8)
...
  refinerycms-authentication (1.0.4)
    devise (~>1.1.8)
...
$ bundle install
...
Using devise (1.1.8)
...
<shell>
$ rails generate refinerycms # --pretend can be helpful before you squash stuff in ExistingApp

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.

$ git checkout app/views/layouts/application.html.erb
$ rm app/views/layouts/application.html.erb.backup
$ mkdir public/stylesheets/hide
$ mv public/stylesheets/*.css public/stylesheets/hide/.
$ mv public/stylesheets/hide/front_end_styles.css public/stylesheets/.
$ git checkout config/initializers/devise.rb #or merge
$ grep -R "user" db/migrate/. #best to look thru the migrations manually as well, to avoid squashing your tables.

With the refinery gem versions used in this ExistingApp’s Gemfile, we need to modify the migrations like this: #{timestamp}_create_refinerycms_authentication_schema.rb

# create_table ::User.table_name ...
 #  ...
 # end
 # add_index User.table_name, ...

 def self.down
   [::User].reject{|m|
     !(defined?(m) and m.respond_to?(:table_name))
   }.each do |model|
     drop_table model.table_name unless "users"==model.table_name #refinerycms doesn't own users table.
   end
 end
$ rm db/migrate/#{timestamp}_change_to_devise_users_table.rb
$ rm db/migrate/#{timestamp}_add_remember_created_at_to_users.rb
$ rm db/migrate/#{timestamp}_remove_password_salt_from_users.rb
$ grep -R "User" db/migrate/. #just to double check, then when satisfied...
$ mv app/models/user.rb app/models/user.rb.backup
$ rake refinery:override model=user

We need to update the user model for refinery before running the migrations, since they rely on User methods.

Merge what you need from refinery’s user model into app/models/user.rb.backup, remove the override and restore it’s filename. For the refinery and devise versions in this guide’s ExistingApp, the additions look like this. app/models/user.rb

class User < Refinery::Core::BaseModel
  ...
  has_and_belongs_to_many :roles
  has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy
  has_friendly_id :member_name, :use_slug => true #just for example, our user model doesn't have a username field, but has a memberName field instead.
  ...
  def authorized_plugins
    plugins.collect { |p| p.name } | Refinery::Plugins.always_allowed.names
  end

  def can_delete?(user_to_delete = self)
    user_to_delete.persisted? and
    id != user_to_delete.id and
    !user_to_delete.has_role?(:superuser) and
    Role[:refinery].users.count > 1
  end

  def add_role(title)
    raise ArgumentException, "Role should be the title of the role not a role object." if title.is_a?(Role)
    roles << Role[title] unless has_role?(title)
  end

  def has_role?(title)
    raise ArgumentException, "Role should be the title of the role not a role object." if title.is_a?(Role)
    roles.any?{|r| r.title == title.to_s.camelize}
  end

end
$ rake db:migrate

Refinery code is expecting the user model to have a username field, so we need to find and override all those instances.

$ cd ~/.rvm/ruby-1.8.7-p174@existing/gems
$ grep -R "username" refinerycms*/.

Most of those “username” occurances will be in parts of refinery involving user administration, which we are going to avoid, because we are assuming ExistingApp is handling user administration. (In this guide, that’s just by using the console to keep things simple.)

For the refinery gem versions used in this guide, it was helpful to override a method in the admin base controller and associated view partial.

$ rake refinery:override controller=admin/base
$ rake refinery:override view=admin/blog/posts/_post

+app/controller/refinery/admin_controller.rb

module Refinery
  class AdminController < ActionController::Base
    #need to override a loadtime modification to this controller that used username column
    def restrict_controller
      if Refinery::Plugins.active.reject { |plugin| params[:controller] !~ Regexp.new(plugin.menu_match)}.empty?
        warn "'#{current_user.memberName}' tried to access '#{params[:controller]}' but was rejected."
        error_404
      end
    end

  end
end

Replace post.author.username with post.author.member_name in app/views/admin/blog/posts/_post.html.erb and app/views/blog/posts/_post.html.erb. If you are including other extensions, just grep around in the gem source for user model fields that you may need to update, and override those views and/or controller methods.

To keep ExistingApp in control of user administration, we’ll block the routes to refinery controllers handling user admin. We also need to ensure that the devise routes point to where ExistingApp is expecting them to (which may be customized), and not to where refinery wants to route them. We want some of the refinery routes defined, just not all. But we don’t want to modify the refinery gems so we can upgrade more easily. The solution is to modify the routes after they are loaded, and there is probably a better way to find and remove the refinery added devise routes than this. But here is a method that worked.

First, to find the routes refinery is adding, recognize that not all are in routes.rb files….

grep -R "routes.draw" $(rvm gemdir)

The next problem is that because refinery devise routes after ExistingApp adds the same routes, recognize_path and url_for won’t agree. ExistingApp’s routes take precedence, but the url_for helpers in the refinery code won’t point to them yet. (The will be a problem for ExistingApp’s custom routes for devise user sessions, for example.)

The fix is to reload the named routes after everything else since ruby follows the “last to define wins” rule for named route url_for methods: config/named_routes_overrides.rb

Rails.application.routes.draw do
  devise_for :users, :controllers=>{:sessions=>"users/sessions"} do
   #any additional user session routes would be defined here
  end

  get "static/home", :as=>"user_root"
end
config/application.rb
module ExistingApp
  class Application < Rails::Application
    ...
    initializer 'add named route overrides' do |app|
      app.routes_reloader.paths << File.expand_path('../named_routes_overrides.rb',__FILE__)
    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 “show welcome” view.

$ rails c
ruby> su = Role.new(:title=>"Superuser")
ruby> su.save
true
ruby> refinery_role = Role.new(:title=>"Refinery")
ruby> refinery_role.save
true
ruby> me=User.first #created above to test devise
ruby> me.roles << su
ruby> me.roles << refinery_role
ruby> me.save
true
ruby> exit

We need to also avoid the show_welcome page in test mode or our ExistingApp existing integration tests will suddenly stop working. app/controllers/application_controller

class ApplicationController < ActionController::Base
  ...
  def show_welcome_page?
    #overriding refinery initialization wizard behavior, so unpopulated test
    #database will successfully run.
    false
  end
  ...
end

Ok. We should be ready to give it a spin. If you try to access http://localhost:3000/refinery (the refinery administrative backend), you should get our custom devise new session view. You should now be able to log in and administer pages and the blog. If you try to administer the users through the users tab, you’ll wind up at the home page for now, or you could remove the tab.

$ rails c
ruby> me=User.first
ruby> me.plugins.where(:name=>"refinery_users").first.destroy
ruby> exit

Hopefully, now that you have seen the details for adding Refinery CMS to an ExistingApp already using Devise, you will have a starting point for dealing with your own unique ExistingApp situations.

4 Adding Your Authentication And Authorization Rules To Refinery CMS Admin Or Front End Pages

You can add before_filters to the Refinery controllers without overriding them completely and locally in your app, by sending an include message with the module to mix in to the controllers.

For example, perhaps your ExistingApp has many users and not all of them should have access to the Refinery Admin area of the application.

That could be accomplished like this, based on whether or not each user has the “Refinery” role set in the database or not:

lib/restrict_refinery_to_refinery_users.rb
module RestrictRefineryToRefineryUsers

  def restrict_refinery_to_refinery_users
    return unless !current_user.try(:has_role?, "Refinery") #current_user.try(:roles).try(:empty?) is another possibility
    redirect_to home_path #this user is not a refinery user because they have no refinery roles.
    return false
  end

end
config/application.rb
module ExistingApp
  class Application < Rails::Application
  ...
    config.before_initialize do
      require 'restrict_refinery_to_refinery_users'
    end

    config.to_prepare do

      #restrict access to refinery page and blog controllers and views to refinery users
      PagesController.send :include, RestrictRefineryToRefineryUsers
      PagesController.send :before_filter, :restrict_refinery_to_refinery_users

      BlogController.send :include, RestrictRefineryToRefineryUsers
      BlogController.send :before_filter, :restrict_refinery_to_refinery_users

      #restrict access to refinery admin controllers and views to refinery users
      Refinery::AdminController.send :include, RestrictRefineryToRefineryUsers
      Refinery::AdminController.send :before_filter, :restrict_refinery_to_refinery_users

      Admin::RefinerySettingsController.send :include, RestrictRefineryToRefineryUsers
      Admin::RefinerySettingsController.send :before_filter, :restrict_refinery_to_refinery_users

    end

Now when you update your refinery gems, your ExistingApp business rules are still in place.