Why?
Refinery CMS provides a polished CMS to integrate seamlessly into your existing rails application.
How?
Refinery breaks its components into individual engines and we can pick and choose which engines we want in our application. Refineries authentication engine uses devise however we want to have complete control over Devise in our Rails application.
List of Engines:
- Authentication
- Core
- Dashboard
- Images
- Pages
- Resources
In order to integrate Refinery with your application we will specifically include all the the Refinery engines EXCEPT the Authentication engine.
Steps:
- Setup Rails Application, Devise and Refinery
- Emulate Refinery User Model
- Override and Initialize
- Test
Setup Rails Application, Devise and Refinery
We will now setup refinery and devise.
Update your Gemfile
#Devise gem 'devise' #Upate jquery-rails. refinerycms-pages depends on jquery-rails 2.0 at the time this article was written gem 'jquery-rails', '~> 2.0.0' #Refinery git 'git://github.com/resolve/refinerycms.git' do gem 'refinerycms-core' #You can leave this out if you like. It's a dependency of the other engines. gem 'refinerycms-dashboard' gem 'refinerycms-images' gem 'refinerycms-pages' gem 'refinerycms-resources' end
Install devise
rails generate devise:install
rails generate devise User
rails generate devise:views
for i in `find app/views/devise -name '*.erb'` ; do html2haml -e $i ${i%erb}haml ; rm $i ; done #(if you're using haml)
Install Refinery
rails g refinery:cms --fresh-installation
If you ran rails g refinery:cms --fresh-installation and get the error 'uninitialized constant Refinery::AuthenticationGenerator.'
Steps to fix:
- Add export BUNDLER_EDITOR=vim to your .bashrc or .profile
- bundle open refinerycms-core
- Navigate to core/lib/generators/refinery/cms/cms_generator.rb
- Go to line 204 and edit this to reflect the pull request to RefineryCMS
- Run rails g refinery:cms --fresh-installation
`run_additional_generators!': uninitialized constant Refinery::AuthenticationGenerator (NameError)
I've issued a pull request to RefineryCMS which can be found here: View Here
bundle open refinerycms-core
Comment out the AuthenticationGenerator (Line 204). and run the command again. bundle open refinerycms-core
Emulate Refinery User Model
Take some time to look over the Refinery authentication User models View Here.
We will need to integrate this model with the model we generated with devise. Update your user model to look like the following.
class User < ActiveRecord::Base
has_and_belongs_to_many :roles, :join_table => :roles_users
has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :plugins
# attr_accessible :title, :body
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 can_delete?(user_to_delete = self)
user_to_delete.persisted? &&
!user_to_delete.has_role?(:superuser) &&
::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
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
def create_first
if valid?
# first we need to save user
save
# add refinery role
add_role(:refinery)
# add superuser role
add_role(:superuser) if ::Role[:refinery].users.count == 1
# add plugins
self.plugins = Refinery::Plugins.registered.in_menu.names
end
# return true/false based on validations
valid?
end
end
Refinery expects our user model to have certain methods. The best way to understand what Refinery expects is to look at their User model. Refinery User Model 2.0 Stable. Please take note of the relations that they specify.
has_and_belongs_to_many :roles, :join_table => :refinery_roles_users has_many :plugins, :class_name => "UserPlugin", :order => "position ASC", :dependent => :destroy
This means we will need to add three new Models. Role, RoleUser and UserPlugin.
Add the Role Model
You can view the refinery role model Here
class Role < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :roles_users
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
Add the RoleUser Model
You can view the refinery Role User model Here
class RoleUser < ActiveRecord::Base belongs_to :role belongs_to :user end
Add the UserPlugin Model
You can view the refinery User Plugin model Here
class UserPlugin < ActiveRecord::Base belongs_to :user attr_accessible :user_id, :name, :position end
Add Migrations for these three new models
You can view the refinery Migrations Here
rails g migration CreateRefinerycmsAuthenticationSchema
class CreateRefinerycmsAuthenticationSchema < ActiveRecord::Migration
def change
# Postgres apparently requires the roles_users table to exist before creating the roles table.
create_table :roles_users, :id => false do |t|
t.integer :user_id
t.integer :role_id
end
add_index :roles_users, [:role_id, :user_id]
add_index :roles_users, [:user_id, :role_id]
create_table :roles do |t|
t.string :title
end
create_table :user_plugins do |t|
t.integer :user_id
t.string :name
t.integer :position
end
add_index :user_plugins, :name
add_index :user_plugins, [:user_id, :name], :unique => true
end
end
Override and Initialize
We're almost there! We just need to override methods that refinery uses for authentication (That live outside the authentication engine). There are the main files that contain methods we need to override.
- core/lib/refinery/application_controller.rb
- authentication/lib/refinery/authenticated_system.rb
- authentication/app/models/refinery/user.rb
Add the following to config/initializers/refinery/user.rb
::Refinery::ApplicationController.module_eval do
def just_installed?
::Role[:refinery].users.empty?
end
def refinery_user_required?
if just_installed? and controller_name != 'users'
redirect_to main_app.new_user_registration_path
end
end
def store_location
session[:return_to] = request.fullpath.sub("//", "/")
end
# Redirect to the URI stored by the most recent store_location call or
# to the passed default.
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
# This just defines the devise method for after sign in to support
# extension namespace isolation...
def after_sign_in_path_for(resource_or_scope)
scope = Devise::Mapping.find_scope!(resource_or_scope)
home_path = "#{scope}_root_path"
respond_to?(home_path, true) ? refinery.send(home_path) : refinery.admin_root_path
end
def after_sign_out_path_for(resource_or_scope)
refinery.root_path
end
def refinery_user?
refinery_user_signed_in? && current_refinery_user.has_role?(:refinery)
end
def refinery_user_signed_in?
user_signed_in?
end
def authenticate_refinery_user!
authenticate_user!
end
def current_refinery_user
current_user
end
protected :store_location, :redirect_back_or_default, :refinery_user?
def self.included(base)
if base.respond_to? :helper_method
base.send :helper_method, :current_refinery_user, :current_user_session,
:refinery_user_signed_in?, :refinery_user?
end
end
end
Override refinery site_bar
We need to first override the refinery site bar and then rename refinery.destroy_refinery_user_session_path to main_app.destroy_user_session_path
rake refinery:override view=refinery/_site_bar*
Edit app/views/refinery/_site_bar.html.erb.
<%= link_to t('.log_out', site_bar_translate_locale_args), main_app.destroy_user_session_path, :id => 'logout', :method => :delete %>
Create User Roles
Refinery by default uses two user roles. These roles are Refinery and Superuser.
#Create User Roles
Role.create(:title => 'Refinery')
Role.create(:title => 'Superuser')
#Assign user roles
user = User.first
user.roles << Role.find_by_title('Refinery')
user.roles << Role.find_by_title('Superuser')
user.save
Known Issues
- Redirect loop when no user exists that has the refinery role. Temporary solution is creating a user with the Refinery Role.
- If you ran rails g refinery:cms --fresh-installation and get the following error then edit core/lib/generators/refinery/cms/cms_generator.rb on line 204. Run 'bundle open refinerycms-core'. Comment out the AuthenticationGenerator and run the command again. Special thanks to @parndt!
Testing
I would HIGHLY recommend writting tests for the above code. The good news is the guys at refinerycms have done most of the work for you! You can look at the available tests for authentication Here!
Download The Example
You can download a working example of this blog post Here!
Resources
Thanks to the fantastic developers working on Refinery CMS! I would also like to source this article for giving the inspiration to write this one!