Skip to content

Latest commit

 

History

History
1115 lines (752 loc) · 39.6 KB

TUTORIAL.textile

File metadata and controls

1115 lines (752 loc) · 39.6 KB

Rails3-Subdomain-Devise

Use this project as a starting point for a Rails 3 application that uses subdomains and authentication. User management and authentication is implemented using Devise.

Tutorial

This tutorial documents each step that you must follow to create this application. Every step is documented concisely, so a complete beginner can create this application without any additional knowledge. However, no explanation is offered for any of the steps, so if you are a beginner, you’re advised to look for an introduction to Rails elsewhere. Refer to the Rails Guides site for help if you are a beginner.

For an introduction to Rails 3 and subdomains, see Ryan Bates’s screencast Subdomains in Rails 3 (a transcription is available from ASCIIcasts).

Use Cases

What Is Implemented

This example implements “blog-style subdomains in Rails.” The example is similar to the application shown in Ryan Bates’s screencast Subdomains in Rails 3 but adds authentication using Devise. In this example, there is a “main” domain where anyone can visit and create a user account. And registered users can create any number of subdomains which could host blogs or other types of sites.

What Is Not Implemented

Another use of subdomains is often called “Basecamp-style subdomains in Rails.” Visitors to the main site can create a user account which is then hosted at a subdomain that matches their user name. Each user has only one subdomain and when they log in, all their activity is confined to their subdomain. A user’s home page and account info is accessed only through the subdomain that matches their user name. This approach is NOT implemented in this application (if you build an example of this, let me know and I will add a link here).

Assumptions

This tutorial is based on Rails 3 and Devise 1.1.1.

Before beginning this tutorial, you need to install

  • The Ruby language (version 1.8.7 or 1.9.2)
  • Rails (version 3 release candidate or newer)
  • A working installation of SQLite (preferred), MySQL, or PostgreSQL

I recommend installing rvm, the Ruby Version Manager, to manage multiple versions of Rails if you have a mix of Rails 3 and Rails 2 applications.

If you are using rvm, you can see a list of the Ruby versions currently installed:
$ rvm list

Check that appropriate versions of Ruby and Rails are installed in your development environment:
$ ruby -v
$ rails -v

If you intend to deploy to Heroku, note that Heroku supports Ruby version 1.8.7 but not Ruby version 1.9.2 (as of 7 August 2010).

Generating the Application

You can work through this tutorial using copy-and-paste to create your application. Alternatively, you can generate a new Rails app:

$ rails new app_name -m http://github.com/fortuity/rails3-subdomain-devise/raw/master/template.rb

This creates a new Rails app (with the app_name you provide) on your computer. It includes everything in the example app as described here. Plus it offers you the option of setting up your view files using the Haml templating language.

If you wish to “change the recipe” to generate the app with your own customized options, you can copy and edit the file template.rb.

Create the Rails Application

If you wish work through this tutorial using copy-and-paste, start by creating a new Rails app.

Open a terminal, navigate to a folder where you have rights to create files, and type:

$ rails new rails3-subdomain-devise

You may give the app a different name. For this tutorial, we’ll assume the name is “rails3-subdomain-devise.”

This application will use a SQLite database for data storage. You may also use MySQL or PostgreSQL for data storage (refer to Getting Started with Rails).

After you create the application, switch to its folder to continue work directly in that application:

$ cd rails3-subdomain-devise

Edit the README file to remove the standard Rails boilerplate. Add what you like (perhaps the name of your app?).

Set Up Source Control (Git)

If you’re creating an app for deployment into production, you’ll want to set up a source control repository at this point. If you are building a throw-away app for your own education, you may skip this step.

Check that git is installed on your computer:

$ git version

Rails 3 has already created a .gitignore file for you. You may want to modify it to add .DS_Store if you are using Mac OS X.

.bundle
db/*.sqlite3
log/*.log
tmp/**/*
.DS_Store

Initialize git and check in your first commit:

$ git init
$ git add .
$ git commit -m 'initial commit'

At this point you can check your local project into a remote source control repository. We’ll assume you are using git with an account at GitHub.

Check that your GitHub account is set up properly:

$ ssh git(at)github.com

Go to GitHub and create a new empty repository (http://github.com/repositories/new) into which you can push your local git repo.

Add GitHub as a remote repository for your project and push your local project to the remote repository:

$ git remote add origin git(at)github.com:YOUR_GITHUB_ACCOUNT/YOUR_PROJECT_NAME.git
$ git push origin master

You can check your commit status at any time with:

$ git status

At each stage of completion, you should check your code into your local repository:

$ git commit -am "some helpful comment"

and then push it to the remote repository:

$ git push origin master

Set Up Gems

Required Gems

The application uses the following gems:

  • rails (version 3.0.0.rc)
  • sqlite3-ruby
  • devise (version 1.1.1)
  • friendly_id (version 3.1.1.1)

The SQLite3 gem is used for the database. You can substitute a different database if you wish.

The FriendlyId gem is used to give users and subdomains easily recognizable strings instead of numeric ids in URLs.

Set up Your Gemfile

Edit the Gemfile file to look like this:

source 'http://rubygems.org'

gem 'rails', '3.0.0.rc'
gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'devise', '1.1.1'
gem 'friendly_id', '3.1.1.1'

Optional Gems

If you wish to deploy to Heroku, you will need to include the following gem:

# uncomment the next line if you wish to deploy to Heroku
# gem 'heroku', '1.9.13', :group => :development

Install the Gems

Install the required gems on your computer:

$ bundle install

If you need to troubleshoot, you can check which gems are installed on your computer with:

$ gem list --local

Test the App

You can check that your app runs properly by entering the command

$ rails server

Or you can use the shortcut:

$ rails s

To see your application in action, open a browser window and navigate to http://localhost:3000/. You should see the Rails default information page.

Stop the server with Control-C.

Configuration for FriendlyId

The FriendlyId gem allows us to use easily recognizable strings instead of numeric ids in URLs. To install a database migration for FriendlyId, run:

$ rails generate friendly_id

Prevent Logging of Passwords

We don’t want passwords written to our log file.

In Rails 3, we modify the file config/application.rb to include:

config.filter_parameters += [:password, :password_confirmation]

Note that filter_parameters is an array.

Create a Home Page

Remove the Default Home Page

Delete the default home page from your application:

$ rm public/index.html

You may also want to modify the file public/robots.txt to prevent indexing by search engines if you plan to have a development version on a publicly accessible server:

# To ban all spiders from the entire site uncomment the next two lines:
User-Agent: *
Disallow: /

Create a Home Controller and View

Create the first page of the application. Use the Rails generate command to create a “home” controller and a “views/home/index” page.

$ rails generate controller home index

If you’re using the default template engine, you’ll find the file:

views/home/index.html.erb

Now, you have to set a route to your home page. Edit the file config/routes.rb and replace:

get "home/index"

with

root :to => "home#index"

Set Up Authentication

Set Up Configuration for Devise

This app uses Devise for user management and authentication. Devise is at http://github.com/plataformatec/devise.

We’ve already installed the Devise gem with the $ bundle install command. Run the generator:

$ rails generate devise:install

which installs a localization file and a configuration file:

config/initializers/devise.rb

Now is a good time to change the value config.mailer_sender in the configuration file.

In its default configuration, Devise needs to send email to register a new user or reset a password. We’ll need to set up action_mailer.

Set up action_mailer in your development environment in the file

config/environments/development.rb

by changing:

# Don't care if the mailer can't send
# config.action_mailer.raise_delivery_errors = false

and adding:

### ActionMailer Config
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
# A dummy setup for development - no deliveries, but logged
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default :charset => "utf-8"

Set up action_mailer in your production environment in the file

config/environments/production.rb

by adding:

config.action_mailer.default_url_options = { :host => 'yourhost.com' }
### ActionMailer Config
# Setup for production - deliveries, no errors raised
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false
config.action_mailer.default :charset => "utf-8"

Generate a Model and Routes for Users

Devise can manage users and administrators separately, allowing two (or more) roles to be implemented differently. For this example, we just implement Users.

Use Devise to generate a model, database migration, and routes for a User:

$ rails generate devise User

Devise will modify the config/routes.rb file to add:

devise_for :users

which provides a complete set of routes for user signup and login. If you run rake routes you can see the routes that this line of code creates.

Customize the Application

Enable Users to Have Names

By default, Devise uses an email address to identify users. We’ll add a “name” attribute as well.

Modify the migration file in db/migrate/ to add:

t.string :name

to add a “name” field to the data table.

Next, we’ll modify the User model to allow a “name” to be included when adding or updating a record.

We’ll also modify the User model so the URL for accessing a user uses a name instead of a number (the FriendlyId gem provides this feature).

You’ll also want to prevent malicious hackers from creating fake web forms that would allow changing of passwords through the mass-assignment operations of update_attributes(attrs) and new(attrs). With the default Rails ActiveRecord, Devise adds

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

We’ll need to add the :name attribute to attr_accessible.

Modify the file models/user.rb to include this:

validates_presence_of :name
validates_uniqueness_of :name, :email, :case_sensitive => false
attr_accessible :name, :email, :password, :password_confirmation, :remember_me
has_friendly_id :name, :use_slug => true, :strip_non_ascii => true

This will allow users to be created (or edited) with a name attribute. When a user is created, a name and email must be present and must be unique (not used before). Note that Devise (by default) will check that the email address and password are not blank.

Create Model and Migration for Subdomains

Each user will be able to register and use a subdomain. We’ll create a Subdomain model that belongs to a User. In this implementation, the Subdomain model just stores a string that is the name of the subdomain. Later, we’ll create another model, the Site, that subclasses Subdomain and is used to create the page that is hosted at a URL using the name of the subdomain.

Generate a model and migration for Subdomains. Since a Subdomain will belong to a user, the “user:references” parameter adds a field “user_id” to the data table to handle the relationship with a User:

$ rails generate model Subdomain name:string user:references

Modify the Subdomain model so the URL for accessing a subdomain uses a name instead of a number (the friendly_id gem provides this feature):

class Subdomain < ActiveRecord::Base
  belongs_to :user
  has_friendly_id :name, :use_slug => true, :strip_non_ascii => true
  validates_uniqueness_of :name, :case_sensitive => false
  validates_presence_of :name
end

When a subdomain is created, it must have a name and the name must be unique.

Enable Users to Own Subdomains

A registered user will access his or her profile on the main site to add or delete subdomains.

Subdomains belong to users, so we have to set up the User side of the relationship. Modify the file models/user.rb to look like this:

class User < ActiveRecord::Base
  has_many :subdomains, :dependent => :destroy
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  validates_presence_of :name
  validates_uniqueness_of :name, :email, :case_sensitive => false
  attr_accessible :name, :email, :password, :password_confirmation, :remember_me
  has_friendly_id :name, :use_slug => true, :strip_non_ascii => true
end

Create a Site Model

We’ll create a Site model as a subclass of the Subdomain model so any visitor can view a subdomain-hosted site. The Site is a simple stub in this application. It can be customized for additional functionality (for example, implementation as a blog).

The Site model is very simple so there’s no need to use a generator. Just create a file app/models/site.rb:

$ touch app/models/site.rb

containing:

class Site < Subdomain
end

Set Up the Database

Create a Database and Run Migrations

Now create an empty database. You can do this by running a rake command:

$ rake db:create

Run the migrations:

$ rake db:migrate

You can take a look at the database schema that’s been created for you:

db/schema.rb

Seed the Database With Users and Subdomains

Edit the file db/seeds.rb and add the following code:

puts 'SETTING UP EXAMPLE USERS'
user1 = User.create! :name => 'First User', :email => 'user@test.com', :password => 'please', :password_confirmation => 'please'
puts 'New user created: ' << user1.name
user2 = User.create! :name => 'Other User', :email => 'otheruser@test.com', :password => 'please', :password_confirmation => 'please'
puts 'New user created: ' << user2.name
puts 'SETTING UP EXAMPLE SUBDOMAINS'
subdomain1 = Subdomain.create! :name => 'foo', :user_id => user1.id
puts 'Created subdomain: ' << subdomain1.name
subdomain2 = Subdomain.create! :name => 'bar', :user_id => user2.id
puts 'Created subdomain: ' << subdomain2.name

Run the rake task to seed the database:

$ rake db:seed

Set Up the Main Domain

Create Customized Views for User Registration

Devise provides a controller and views for registering users. It is called the “registerable” module. The controller and views are hidden in the Devise gem so we don’t need to create anything. However, because we want our users to provide a name when registering, we will create custom views for creating and editing a user. Our custom views will override the Devise gem defaults.

First, to copy all the default Devise views to your application, run

rails generate devise:views

This will generate a set of views in the directory app/views/devise/.

Next, modify the views to create and edit users.

Add the following code to each file:

app/views/devise/registrations/edit.html.erb

  <p><%= f.label :name %><br />
  <%= f.text_field :name %></p>

app/views/devise/registrations/new.html.erb

  <p><%= f.label :name %><br />
  <%= f.text_field :name %></p>

We do not need to add a controller with methods to create a new user or edit or delete a user. We use the existing “registerable” module from Devise which provides a controller with methods to create, edit or delete a user.

Note that Devise’s default behaviour allows any logged-in user to edit or delete his or her own record (but no one else’s). When you access the edit page you are editing just your info, and not info of other users.

Create a Controller and Views to Display Users

The site’s home page has no subdomain. We want to add a list of users to the home page. And we want links to pages that shows details about each user. Later we’ll add a list to the details page that shows which subdomains they own.

Create a controller to display users:

$ rails generate controller Users index show

You’ll need to modify the Users controller:

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

end

Next, modify the views to display users. Add the following code to each file:

app/views/users/index.html.erb

<h1>Users</h1>
<table>
<% @users.each do |user| %>
  <tr>
    <td><%= link_to user.name, user %></td>
  </tr>
<% end %>
</table>

app/views/users/show.html.erb

<h1><%= @user.name %></h1>
<p><%= @user.email %></p>
<br />
<p><%= link_to 'Edit', edit_user_registration_path %></p>
<p><%= link_to 'List of Users', users_path %></p>

Implement Routing for the Main Domain

Add a route in the file config/routes.rb to display the users. Your file should look like this:

 devise_for :users
 resources :users, :only => [:index, :show]
 root :to => "home#index"

Note that devise_for :users must be placed above resources :users, :only => [:index, :show] or else you will get errors.

Add a Link to the Main Domain Home Page

We want a link to a list of users on the application home page.

Modify the file:

app/views/home/index.html.erb

with these changes:

<h1>Rails3-Subdomain-Devise</h1>
<p><%= link_to "View List of Users", users_path %></p>

Add Devise Navigation Links

You will want to add navigation links to the application layout for the Devise sign-up and log-in actions. You’ll find a simple example on the Devise wiki.

First, create a folder app/views/devise/menu.

Create the file app/views/devise/menu/_login_items.html.erb and add:

<% if user_signed_in? %>
  <li>
  <%= link_to('Logout', destroy_user_session_path) %>        
  </li>
<% else %>
  <li>
  <%= link_to('Login', new_user_session_path)  %>  
  </li>
<% end %>

Create the file app/views/devise/menu/_registration_items.html.erb and add:

<% if user_signed_in? %>
  <li>
  <%= link_to('Edit account', edit_user_registration_path) %>
  </li>
<% else %>
  <li>
  <%= link_to('Sign up', new_user_registration_path)  %>
  </li>
<% end %>

Create a Stylesheet

Create a public/stylesheets/application.css file and add some menu styling to the css (here for a horizontal menu for navigation links):

ul.hmenu {
  list-style: none;	
  margin: 0 0 2em;
  padding: 0;
}

ul.hmenu li {
  display: inline;  
}

Set up the Application Layout

Add the navigation links to your layouts/application.html.erb file.

And include flash messages to show application alerts and notices.

Your file should look like this:

 
<!DOCTYPE html>
<html>
<head>
  <title>Rails3-Subdomain-Devise</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :defaults %>
  <%= csrf_meta_tag %>
</head>
<body>
  <ul class="hmenu">
    <%= render 'devise/menu/registration_items' %>
    <%= render 'devise/menu/login_items' %>
  </ul>
  <p style="color: green"><%= notice %></p>
  <p style="color: red"><%= alert %></p>
  <%= yield %>
</body>
</html>

Test the Application

You can check that your app runs properly by entering the command:

$ rails server

If you launch the application and visit
http://localhost:3000/
you can click a link to register as a new user. The app is configured to require a new user to confirm registration by clicking a link in an email message. The app’s development environment is set up to log email messages instead of attempting to send them. Check your console or log file for a log entry that contains the text of the email message with the URL you can use to confirm the new user.

It will look something like this:
http://localhost:3000/users/confirmation?confirmation_token=b7iljFz77_3Sp6CftdFa

Visit the confimation URL in your web browser to complete registration of a new user.

Set Up Subdomains

Create a Controller and Views to Manage Subdomains

Each registered user will be able to create any number of subdomains which will be hosts for the user’s “sites.” This app does not provide any functionality for a user’s “sites,” but you can add functionality so each user can have a blog or other features for their “site.”

Create a controller to manage subdomains:

$ rails generate scaffold_controller Subdomains

with the following code in the file:

app/controllers/subdomains_controller.rb

class SubdomainsController < ApplicationController
  before_filter :authenticate_user!, :except => [:index, :show]
  before_filter :find_user, :except => [:index, :show]
  respond_to :html

  def index
    @subdomains = Subdomain.all
    respond_with(@subdomains)
  end

  def show
    @subdomain = Subdomain.find(params[:id])
    respond_with(@subdomain)
  end

  def new
  @subdomain = Subdomain.new(:user => @user)
  respond_with(@subdomain)
  end

  def create
    @subdomain = Subdomain.new(params[:subdomain])
    if @subdomain.save
      flash[:notice] = "Successfully created subdomain."
    end
    redirect_to @user
  end

  def edit
    @subdomain = Subdomain.find(params[:id])
    respond_with(@subdomain)
  end

  def update
    @subdomain = Subdomain.find(params[:id])
    if @subdomain.update_attributes(params[:subdomain])
      flash[:notice] = "Successfully updated subdomain."
    end
    respond_with(@subdomain)
  end

  def destroy
    @subdomain = Subdomain.find(params[:id])
    @subdomain.destroy
    flash[:notice] = "Successfully destroyed subdomain."
    redirect_to @user
  end

  protected

    def find_user
      if params[:user_id]
        @user = User.find(params[:user_id])
      else
        @subdomain = Subdomain.find(params[:id])
        @user = @subdomain.user
      end
      unless current_user == @user
        redirect_to @user, :alert => "Are you logged in properly? You are not allowed to create or change someone else's subdomain."
      end
    end

end

Create views to manage subdomains, with the following code in each file:

app/views/subdomains/_form.html.erb

<% if @subdomain.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@subdomain.errors.count, "error") %> prohibited this subdomain from being saved:</h2>
    <ul>
    <% @subdomain.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
<%= fields_for @subdomain do |f| %> 
  <div>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <%= f.hidden_field (:user_id, :value => @subdomain.user_id) %>
  </div>
  <br />
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

app/views/subdomains/edit.html.erb

<h1>Editing subdomain</h1>
<%= form_for(@subdomain) do |f| %>
  <%= render 'form' %>
<% end %><%= link_to 'Show', @subdomain %> |
<%= link_to @subdomain.user.name, user_path(@subdomain.user) %>

app/views/subdomains/index.html.erb

<h1>Subdomains</h1>
<table>
<% @subdomains.each do |subdomain| %>
  <tr>
    <td><%= link_to subdomain.name, subdomain %></td>
    <td>(belongs to <%= link_to subdomain.user.name, user_url(subdomain.user) %>)</td>
    <td><%= link_to 'Edit', edit_subdomain_path(subdomain) %></td>
    <td><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

app/views/subdomains/new.html.erb

<h1>New subdomain</h1>
<%= form_for([@user, @subdomain]) do |f| %>
  <%= render 'form' %>
<% end %>
<%= link_to @subdomain.user.name, user_path(@subdomain.user) %>

app/views/subdomains/show.html.erb

<h1><%= @subdomain.name %></h1>
<p>Belongs to: <%= link_to @subdomain.user.name, user_url(@subdomain.user) %></p>
<%= link_to 'Edit', edit_subdomain_path(@subdomain) %>

Modify the User’s Detail Page to List Subdomains

Show a list of subdomains on the user’s detail page. Edit the file app/views/users/show.html.erb to look like this:

<h1><%= @user.name %></h1>
<p>Email: <%= @user.email %></p>
<%= link_to 'Edit', edit_user_registration_path %> |
<%= link_to 'List of Users', users_path %>
<h3><%= @user.name %>'s Subdomains</h3>
<table>
<% @user.subdomains.each do |subdomain| %>
  <tr>
    <td><%= link_to subdomain.name, subdomain %></td>
    <td><%= link_to 'Edit', edit_subdomain_path(subdomain) %></td>
    <td><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>
<br />
<%= link_to "Add New Subdomain", new_user_subdomain_path(@user) %>

Implement Routing for Subdomains

Add a route for subdomains in the file config/routes.rb:

 devise_for :users
 resources :users, :only => [:index, :show] do
   resources :subdomains, :shallow => true
 end
 root :to => "home#index"

We use “shallow routes” to simplify the URLs. Routes are built only with the minimal amount of information that is needed to uniquely identify the resource. So instead of:

user_subdomain_url(subdomain.user,subdomain) #=> '/users/firstuser/subdomain/foo'

we can use:

subdomain_url(subdomain) #=> '/subdomains/foo'

Create a Controller and Views to Display Subdomain Sites

In this step, we will create a simple stub that displays a “site” page as the home page of any registered subdomain.

Create a controller to display sites:

$ rails generate controller Sites show

You’ll need to modify the Sites controller:

app/controllers/sites_controller.rb

class SitesController < ApplicationController
  def show
    @site = Site.find_by_name!(request.subdomain)
  end

end

Add the following code to the file:

app/views/sites/show.html.erb

<h1>Site: <%= @site.name %></h1>
<p>Belongs to: <%= link_to @site.user.name, user_url(@site.user) %></p>

Implement Routing for Sites

At this point you can use the Rails 3 built-in routing support for subdomains.

Edit the file config/routes.rb to look like this:

devise_for :users
resources :users, :only => [:index, :show] do
  resources :subdomains, :shallow => true
end
match '/' => 'sites#show', :constraints => { :subdomain => /.+/ }
root :to => "home#index"

Test the Application With Subdomains

If you launch the application, it will be running at http://localhost:3000/. However, unless you’ve made some configuration changes to your computer, you won’t be able to resolve an address that uses a subdomain, such as http://foo.localhost:3000/. There are several complex solutions to this problem. You could set up your own domain name server on your localhost and create an A entry to catch all subdomains. You could modify your /etc/hosts file (but it won’t accommodate dynamically created subdomains). You can create a proxy auto-config file and set it up as the proxy in your web browser preferences. There’s a far simpler solution that does not require reconfiguring your computer or web browser preferences. The developer Levi Cook registered a domain, lvh.me (short for: local virtual host me), that resolves to the localhost IP address 127.0.0.1 and supports wildcards (accommodating dynamically created subdomains). See Tim Pope’s blog post for a NSFW alternative.

To test the application, visit http://lvh.me:3000/. You can also try http://foo.lvh.me:3000/ or http://bar.lvh.me:3000/.

Add Navigation Between Sites

Applications that do not use subdomains use routing helpers to generate links that either include the site’s hostname (for example, users_url generates http://mysite.com/users) or links that only contain a relative path (for example, users_path generates /users).

To provide navigation between sites hosted on the subdomains and the main site, you must use URL helpers (“users_url”) not path helpers (“users_path”) because path helpers do not include a hostname. You’ll also need to find a way to include a subdomain as part of the hostname when generating links. You can specify a hostname when creating a link, with the syntax:

root_url(nil, {:host => "subdomain.somedomain.com"})

but this will require you to hardcode the name of the host into every link. Ideally, we should be able to pass a :subdomain option to the url helper, like this:

root_url(:subdomain => @subdomain.name)

In his screencast Subdomains in Rails 3, Ryan Bates makes some useful suggestions for improved handling of URLs that contain subdomains. He shows how to write a helper that constructs a URL with an optional subdomain parameter.

In a June 19, 2010 blog post on Custom Subdomains in Rails 3, Brian Cardarella makes similar suggestions (you may want to compare his implementation with Ryan Bates’s).

Overwriting the URL Helper

Ryan Bates suggests the following. Create a app/helpers/url_helper.rb file with the following code:

module UrlHelper
  def with_subdomain(subdomain)
    subdomain = (subdomain || "")
    subdomain += "." unless subdomain.empty?
    [subdomain, request.domain, request.port_string].join
  end

  def url_for(options = nil)
    if options.kind_of?(Hash) && options.has_key?(:subdomain)
      options[:host] = with_subdomain(options.delete(:subdomain))
    end
    super
  end
end

and modify the file app/controllers/application_controller.rb to look like this:

class ApplicationController < ActionController::Base
  include UrlHelper
  protect_from_forgery
end

Use Subdomain-Specific Links

To provide navigation between sites hosted on the subdomains and the main site, you’ll need to use the custom URL helpers we created above.

To add links from a subdomain-hosted site to the main site, modify the code in the file:

app/views/sites/show.html.erb

<h1>Site: <%= @site.name %></h1>
<p>Belongs to: <%= link_to @site.user.name, user_url(@site.user, :subdomain => false) %></p>
<p><%= link_to 'Home', root_url(:subdomain => false) %></p>

You can also modify the users’ details pages to include links to their sites.

Edit the file app/views/users/show.html.erb to look like this:

<h1><%= @user.name %></h1>
<p>Email: <%= @user.email %></p>
<%= link_to 'Edit', edit_user_registration_path %> |
<%= link_to 'List of Users', users_path %>
<h3><%= @user.name %>'s Subdomains</h3>
<table>
<% @user.subdomains.each do |subdomain| %>
  <tr>
    <td><%= link_to subdomain.name, subdomain %></td>
    <td><%= link_to 'Edit', edit_subdomain_path(subdomain) %></td>
    <td><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %></td>
    <td><%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %></td>
  </tr>
<% end %>
</table>
<br />
<%= link_to "Add New Subdomain", new_user_subdomain_path(@user) %>

Prevent Use of the Main Site With Subdomain URLs

Though there are no links to main site pages such as user profiles on the subdomain-hosted sites, it’s possible for a clever visitor to enter a URL such as http://foo.lvh.me:3000/users/. We want to segregate the functionality of the main site and make sure it is only accessible when there is no subdomain attached to the hostname. There are several ways to accomplish this. One approach is to set a before_filter in the application controller.

Modify the file app/controllers/application_controller.rb to look like this:

class ApplicationController < ActionController::Base
  include UrlHelper
  protect_from_forgery
  before_filter :limit_subdomain_access

  protected

    def limit_subdomain_access
        if request.subdomain.present?
          # this error handling could be more sophisticated!
          # please make a suggestion :-)
          redirect_to root_url(:subdomain => false)
        end
    end

end

To allow access to the subdomain-hosted sites, override the before_filter by adding the following code in the only controller that should allow access via subdomains:

app/controllers/sites_controller.rb

The top few lines will look like this:

class SitesController < ApplicationController
  skip_before_filter :limit_subdomain_access

  def show
    @site = Site.find_by_name!(request.subdomain)
  end

end

Ignore a “www” Subdomain

In his screencast Subdomains in Rails 3, Ryan Bates shows how to create a routing constraint that will redirect a “www” URL to the main home page. Unfortunately the implementation doesn’t work in the Rails 3 release candidate. If you have a solution or workaround, please open an issue on GitHub and I will update this example to include it.

Maintaining Sessions Across Subdomains

The application has a significant limitation as implemented so far. A user can log in to the main site but will not be logged in when they visit a subdomain-hosted site. That’s because session data for each visitor is managed by browser-based cookies and cookies are not shared across domains (and, by default, not shared across subdomains). Not only is the user login not maintained between the main site and subdomains, but flash messages (used to communicate between actions) are lost because they are stored in sessions.

Examine the Issue

You can see this limitation by displaying the current_user on each page.

Modify the file app/views/devise/menu/_login_items.html.erb to look like this:

<% if user_signed_in? %>
  <li>
  <%= link_to('Logout', destroy_user_session_path) %>        
  </li>
<% else %>
  <li>
  <%= link_to('Login', new_user_session_path)  %>  
  </li>
<% end %>
<li>
  User: 
  <% if current_user %>
    <%= current_user.name %>
  <% else %>
    (not logged in)
  <% end %>
</li>

When you first visit the main site, a cookie is set to maintain the session. When you log in, the current_user value is set and will be displayed on each page. If you visit a subdomain-hosted page, a new session is created, a new cookie is set (corresponding to the subdomain hostname), and no current_user value is present.

Test the app by logging in to the main site and then visiting a subdomain-hosted site. You’ll see the user login is lost when visiting the subdomain-hosted site.

If you examine the browser’s cookies, you will see that a separate session is stored for each different subdomain you’ve visited.

Troubleshooting Cookies

If you’re using Firefox as your web browser, you can use the Firecookie extension for Firebug to examine and remove cookies.

If you remove cookies for the site, you can visit any pages that don’t require session data. But as soon as you try to login (after submitting the login form), you will see an error:

ActionController::InvalidAuthenticityToken in Devise/sessionsController#create

The error arises because the authenticity token used by the “protect_from_forgery” feature (in the application controller) is stored in session data which requires a cookie.

Allow Cookies To Be Shared Across Subdomains

To maintain sessions between the main site and subdomain-hosted sites, modify the configuration file to add the parameter :domain => :all:

config/initializers/session_store.rb

Rails3SubdomainDevise::Application.config.session_store :cookie_store, :key => '_rails3-subdomain-devise_session', :domain => :all

Alternatively, we can use the domain name of the deployed application (with a prepended dot), such as :domain => ".mysite.com". However, by using the parameter :domain => :all we don’t have to hardcode the domain name in the app and we can test the app on our localhost machine.

Issues With the Rails 3 Release Candidate, Cookies, and Subdomains

The Rails 3 release candidate does not behave as expected when :domain => :all is set in config/initializers/session_store.rb.

Before investigating this issue, clear all cookies from your browser.

Setting :domain => :all gives us an error “ActionController::InvalidAuthenticityToken in Devise/sessionsController#create” when we visit http://lvh.me:3000/ and attempt to log in. This is an issue with the Rails 3 release candidate, not with Devise.

To demonstrate that this is an issue with the Rails 3 release candidate, we can create another form:

rails generate scaffold Widget name:string

Clear all cookies and restart the server. Setting :domain => :all and submitting a form to create a Widget generates the “ActionController::InvalidAuthenticityToken” error. If we examine cookies, we can see none are set for this domain.

Compare this with setting :domain => ".lvh.me". Clear all cookies and restart the server. Submitting a form to create a Widget does not generate an error. Logging in with Devise does not generate an error. And we can visit a subdomain-hosted site such as http://foo.lvh.me:3000/ and the current_user remains available in the session. If we examine cookies, we’ll see a session cookie is present for the domain “.lvh.me”.

In conclusion, the Rails 3 release candidate works as expected with the setting :domain => ".lvh.me" but not :domain => :all.

Workaround: As a workaround, use :domain => ".lvh.me" for development and then change it when you deploy it elsewhere.

Issue Resolved in Rails 3 Master

This issue is resolved with the Rails 3 master on GitHub. See commit fd78bb72704554737117 by Bryce Thornton. You can install the Rails 3 master from GitHub by changing your Gemfile and running bundle install.

gem 'rails', :git => 'git://github.com/rails/rails.git' 
#gem 'rails', '3.0.0.rc'

After you run bundle install you can should see the latest commit number from the master Rails repo in the Gemfile.lock file.

Conclusion

This concludes the tutorial for creating a Rails 3 web application that uses subdomains and provides user management and authentication using Devise.

Credits

Daniel Kehoe (http://danielkehoe.com/) implemented the application and wrote the tutorial.

Was this useful to you? Follow me on Twitter:
http://twitter.com/danielkehoe
and tweet some praise. I’d love to know you were helped out by the tutorial.

Contributors

A big thank you to contributor Fred Schoeneman for improving the tutorial.