-
Notifications
You must be signed in to change notification settings - Fork 24
Tutorial (walkthrough)
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.
To keep up to date with development of this app, follow me on Twitter:
http://twitter.com/railsinit.
Any issues? Please create an Issue on GitHub.
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).
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.
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.
For an implementation of “Basecamp-style subdomains in Rails,” see:
Steve Alex’s Basecamp-style fork.
Sachin Sagar Rai (millisami) has revised Steve Alex’s Basecamp-style fork to use Mongodb with Mongoid:
Millisami’s Basecamp-style fork with Mongoid.
(Got your own? Contact me and I will add it here.)
No testing (RSpec or otherwise) is implemented. This app only serves to demonstrate Devise working with subdomains on Rails 3.
Before beginning this tutorial, you need to install
- The Ruby language (version 1.8.7 or 1.9.2)
- Rails (version 3.0.4)
- 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 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
To create the application, you can follow each step in this tutorial and cut and paste the code into your own files. However, it may be easier to generate a new Rails app using the example as a template:
$ rails new app_name -m https://github.com/fortuity/rails3-application-templates/raw/master/rails3-subdomain-devise-template.rb
You MUST be using Rails 3.0.4. Generating a Rails application from an “HTTPS” URL does not work in Rails 3.0.3 and earlier versions.
This creates a new Rails app (with the app_name
you provide) on your computer. It includes everything in the example app. You can read through the tutorial with the code already on your computer.
If you wish to “change the recipe” to generate the app with your own customized options, you can copy and edit the file rails3-mongoid-devise-template.rb found at the project fortuity/rails3-application-templates.
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?).
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
The application uses the following gems.
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.
I recommend checking for newer versions of these gems before proceeding:
- rails (version 3.0.4) (check rubygems.org for the rails gem)
- devise (version 1.2.rc) (Check rubygems.org for the devise gem)
- friendly_id (version 3.2.1) (Check rubygems.org for the friendly_id gem)
The app has been tested with the indicated versions. If you are able to build the app with a newer gem, please create an issue on GitHub and I will update the app.
Edit the Gemfile file to look like this:
source 'http://rubygems.org' gem 'rails', '3.0.4' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'devise', '1.2.rc' gem 'friendly_id', '3.2.1'
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', :group => :development
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
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.
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
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.
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 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"
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"
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.
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.
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.
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
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
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
Edit the file db/seeds.rb and add the following code:
puts 'SETTING UP EXAMPLE USERS' user1 = User.create! :name => 'First User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please' puts 'New user created: ' << user1.name user2 = User.create! :name => 'Other User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please' puts 'New user created: ' << user2.name puts 'SETTING UP EXAMPLE SUBDOMAINS' subdomain1 = Subdomain.create! :name => 'foo' puts 'Created subdomain: ' << subdomain1.name subdomain2 = Subdomain.create! :name => 'bar' puts 'Created subdomain: ' << subdomain2.name user1.subdomains << subdomain1 user1.save user2.subdomains << subdomain2 user2.save
Run the rake task to seed the database:
$ rake db:seed
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.
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>
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.
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>
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, :method => :delete) %> </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 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; }
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>
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.
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) %>
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) %>
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'
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>
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"
If you launch the application, it will be running at http://localhost:3000/ or http://0.0.0.0: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/.
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).
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
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) %>
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
Devise sends registration confirmations, forgotten password messages, and other email messages. These will originate from the main domain. You can modify the application so these emails originate from subdomains.
Add the following code to the file app/helpers/url_helper.rb:
def set_mailer_url_options ActionMailer::Base.default_url_options[:host] = with_subdomain(request.subdomain) end
and modify the file app/controllers/application_controller.rb to add:
before_filter :set_mailer_url_options
(Thanks to Tom Howlett for this contribution.)
At this point, if we visit http://www.lvh.me:3000/ our application will look for a site with the subdomain “www” which doesn’t exist. Instead this should redirect to the main home page.
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. His solution requires writing an auxiliary class but there’s a simpler solution.
Here’s how to create a routing constraint that will redirect a “www” URL to the main home page.
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 '/' => 'home#index', :constraints => { :subdomain => 'www' } match '/' => 'sites#show', :constraints => { :subdomain => /.+/ } root :to => "home#index"
This will intercept the “www” subdomain first, then the limit_subdomain_access
method in the ApplicationController will redirect to root, effectively getting rid of the “www”. Note that you will need to add your own code to prevent users from creating a subdomain called “www”.
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.
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.
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.
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.
For your convenience, here are instructions for deploying your app to Heroku. Heroku provides low cost, easily configured Rails application hosting.
To deploy this app to Heroku, you must have a Heroku account. If you need to obtain one, visit http://heroku.com/ to set up an account.
Make sure the Heroku gem is in your Gemfile. If it’s not, add it and run
$ bundle install
to set up your gems again.
If you’ve just created a Heroku account, add your public key immediately after installing the heroku gem so that you can use git to push or clone Heroku app repositories. See http://docs.heroku.com/heroku-command for details.
Use the Heroku create command to create and name your new app> Specify the “bamboo-ree-1.8.7” stack so Heroku will allow you to use Rails 3.
$ heroku create _myapp_ --stack bamboo-ree-1.8.7
You will need the following Heroku add-ons to deploy your app using subdomains with your own custom domain:
- Custom Domains (free)
- Custom Domains + Wildcard ($5 per month)
- Zerigo DNS Tier 1 ($7 per month)
To enable the add-ons, you can use the Heroku web interface or you can enter the following commands:
$ heroku addons:add custom_domains
$ heroku domains:add mydomain.com
$ heroku addons:add wildcard_domains
$ heroku domains:add *.mydomain.com
$ heroku addons:add zerigo_dns:tier1
If you are using the Zerigo DNS service, you will need to set the nameserver with your domain registrar. It can take a few minutes (or longer) for DNS changes to propagate. When DNS is set properly, you should be able to visit mydomain.com or test.mydomain.com in your web browser and see the Heroku default page:
Heroku | Welcome to your new app!
You can check that everything has been added correctly by running:
$ heroku info --app myapp
Push your application to Heroku:
$ git push heroku master
Set up your Heroku database:
$ heroku rake db:migrate
Initialize your application database:
$ heroku rake db:seed
If you use the heroku command to open your default web browser to your site with
$ heroku open
or if you visit your site with http://myapp.heroku.com/ you’ll see the error, “The page you were looking for doesn’t exist.” That’s because your app is trying to find a subdomain “myapp” that doesn’t exist. You’ll need to visit the site using your own domain name, such as http://mydomain.com/. Domain name service must be set properly to use the Zerigo nameservers.
If you get errors, you can troubleshoot by reviewing the log files:
$ heroku logs
This concludes the tutorial for creating a Rails 3 web application that uses subdomains and provides user management and authentication using Devise.
Daniel Kehoe (http://danielkehoe.com/) implemented the application and wrote the tutorial.
Was this useful to you? Follow me on Twitter:
http://twitter.com/railsinit
and tweet some praise. I’d love to know you were helped out by the tutorial.
Thank you to contributor Fred Schoeneman for improving the tutorial.
Thank you to contributor Charlie Ussery for suggesting how to ignore the “www” subdomain.
Thank you to contributor Tom Howlett for suggesting how to use subdomains in Devise emails.
Thank you to contributor Tom Mornini for improvements to forms.