Skip to content

Tutorial (Walkthrough)

fortuity edited this page Sep 12, 2010 · 32 revisions

Subdomain-Authentication

You can use this project as a starting point for any Rails web application that requires subdomains and authentication. User management and authentication is implemented using Devise. The subdomain_routes gem implements subdomains and routing.

UPDATE: In Rails 3 it’s much easier to implement subdomains than in Rails 2 (no plugin required). For a complete example implementation of Rails 3 subdomains with authentication, using Devise, with a detailed tutorial, see http://github.com/fortuity/rails3-subdomain-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.

Use Cases

  1. Administrators can visit the “admin” subdomain and view an administrative home page or list of administrators.
  2. Only an administrator can create a new administrator (no email confirmation is needed).
  3. Any administrator can change or delete another administrator.
  4. Visitors to the main application (without a subdomain) can register as users and create subdomains.
  5. Any visitor can visit a subdomain and see a “site” home page.

Assumptions

This tutorial is based on Rails version 2.3.5. Some of the code shown here will not work in older versions of Rails.

This was written before the release of Rails 3.0. Things will change significantly in Rails 3.0 so this tutorial will be outdated when Rails 3.0 is released.

Before beginning this tutorial, you need to install

  • The Ruby language (version 1.8.7 or newer)
  • The RubyGems packaging system (version 1.3.5 or newer)
  • A working installation of SQLite (preferred), MySQL, or PostgreSQL
  • Rails (version 2.3.5 or newer)

Check that current versions are installed on your computer:
$ ruby -v
$ gem -v
$ rails -v

You should have experience building a simple Rails application. Refer to Rails Guides for help if you are a beginner.

Create the Rails Application

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

$ rails subdomain-authentication

You may give the app a different name if you are building it for your own use. For this tutorial, we’ll assume the name is “subdomain-authentication.”

This will create a Rails application that uses 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 subdomain-authentication

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

Create a .gitignore file containing:

.DS_Store
log/*.log
tmp/**/*
config/database.yml
config/initializers/site_keys.rb
db/*.sqlite3

Initialize git and check in your first commit:

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

You can check your commit status at any time with:

$ git status

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

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

$ git commit -a -m "some helpful comment"

and then push it to the remote repository:

$ git push origin master

Set Up Gems

About Required Gems

The application uses the following gems:

  • haml (version 2.2.17)
  • will_paginate (version 2.3.12)
  • formtastic (version 0.9.7)
  • warden (version 0.9.5)
  • devise (version 1.0.3)
  • inherited_resources (version 1.0.3)
  • subdomain_routes (version 0.3.1)
  • friendly_id (version 2.2.7)

Note that the devise gem must be version 1.0.3. Newer versions of this gem only support Rails 3 and are NOT backward compatible. The instructions below force your app to use the correct version of this gem.

Note that the inherited_resources gem must be version 1.0.3. Newer versions of the inherited_resources gem only support Rails 3 and are NOT backward compatible. The instructions below force your app to use inherited_resources version 1.0.3 and not the newer version. If you launch the app and get the error

uninitialized constant Rails::Railtie

you are using a newer version of the inherited_resources gem.

Install the Required Gems

You can check which gems are installed on your computer with:

$ gem list --local

If any gems need updating, you can update all with

$ sudo gem update

or individually, as in this example:

$ sudo gem update haml
$ sudo gem update will_paginate
$ sudo gem update formtastic
$ sudo gem update devise
$ sudo gem update warden
$ sudo gem update inherited_resources
$ sudo gem update subdomain_routes
$ sudo gem update friendly_id

If they are not already installed, install the required gems on your computer:

$ sudo gem install haml
$ sudo gem install will_paginate
$ sudo gem install formtastic
$ sudo gem install warden
$ sudo gem install devise --version=1.0.3
$ sudo gem install inherited_resources --version=1.0.3
$ sudo gem install subdomain_routes
$ sudo gem install friendly_id

Keep in mind that you have installed these gems locally. When you deploy the app to another server, the same gems (and versions) must be available.

Specify the Required Gems

Modify the environment.rb file to specify the required gems:

# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.3.4' unless defined? RAILS_GEM_VERSION

# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  config.gem 'haml', :lib => 'haml', :version => '>=2.2.17'
  config.gem 'will_paginate', :source => 'http://gemcutter.org', :version => '>= 2.3.12'
  config.gem 'formtastic', :source => 'http://gemcutter.org/', :version => '>= 0.9.7'
  config.gem 'warden', :source => 'http://gemcutter.org/', :version => '0.9.5'
  config.gem 'devise', :source => 'http://gemcutter.org/', :version => '1.0.3'
  config.gem "inherited_resources", :lib => "inherited_resources", :source => "http://gemcutter.org/", :version => '1.0.3'
  config.gem "subdomain_routes", :source => "http://gemcutter.org", :version => '>= 0.3.1'
  config.gem 'friendly_id', :version => '>= 2.2.7'

  config.time_zone = 'UTC'
end

Create a Gems Manifest for Heroku

If you’re going to be deploying your app to the Heroku hosted platform, this is an opportune time to create a gems manifest for Heroku. If you do not intend to deploy to Heroku, you can skip this step.

Create a top, project-level .gems file containing:

haml --version '>= 2.2.17'
will_paginate --version '>= 2.3.12' --source http://gemcutter.org
formtastic --version '>= 0.9.7' --source http://gemcutter.org
warden --version '0.9.5' --source http://gemcutter.org
devise --version '1.0.3' --source http://gemcutter.org
inherited_resources --version '1.0.3' --source http://gemcutter.org
subdomain_routes --version '>= 0.3.1' --source http://gemcutter.org
friendly_id --version '>= 2.2.7'

Additional Gems for Development

The following gems are not needed for the application to run but they will be used during development. Install them now if they are not already installed:

$ sudo gem install nifty-generators
$ sudo gem install dry_scaffold

Set Up Templating, Layout, and Stylesheets

Enable Haml

To enable Haml for the application, run:

$ haml --rails .

(Be sure to include the trailing dot to specify the path to the current directory.)

This will create a Haml plugin in vendor/plugins. After Haml is initialized, all view files with the .html.haml extension will be compiled using Haml when the application is launched.

Create an Application Layout

Create application layout, stylesheet, and helper files.

$ script/generate nifty_layout --haml

This will create the default layout for every page in the application:

app/views/layouts/application.html.haml

As well as a layout helper file with helper methods for page title and stylesheet includes:

app/helpers/layout_helper.rb

and an initial SASS format stylesheet file:

public/stylesheets/sass/application.sass

Modify the Application SASS Stylesheet

Current Rails practice is to use flash_notice and flash_alert as the CSS identifiers for application messages. The SASS application stylesheet file generated by nifty_layout uses flash_error instead of flash_alert. You’ll need to change the file:

public/stylesheets/sass/application.sass

replacing flash_error with flash_alert.

Add Stylesheets

You might wait until after you’ve built your application to begin applying CSS styling to your pages. Or you can do it now and your pages will look more visually appealing during development.

This application uses HAML and its companion SASS to to mark up pages for layout and CSS styling. We also use the formtastic gem in conjunction with a SASS version of formtastic to mark up forms. The SASS version of formtastic is available here: http://github.com/activestylus/formtastic-sass.

In the directory for the SASS files:

public/stylesheets/sass

add SASS stylesheet files for formtastic styling:

http://github.com/activestylus/formtastic-sass/blob/master/_formtastic_base.sass
http://github.com/activestylus/formtastic-sass/blob/master/_skintastic.sass
http://github.com/activestylus/test_formtastic_sass/blob/master/public/stylesheets/sass/_scaffold.sass

Modify the application SASS file to import the formtastic SASS files:

public/stylesheets/sass/application.sass

Add this at the beginning of the application SASS file:

@import scaffold.sass
@import formtastic_base.sass
@import skintastic.sass

As you develop your app, you can refer to http://github.com/activestylus/formtastic-sass for instructions on modifying the styling of forms.

When you launch your web app, HAML will automatically generate an application.css file from the component SASS files.

Create a Home Page

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.

$ script/generate controller home index

There’s no option in Rails 2.3.5 to generate Haml instead of erb view files, so you will have to delete:

app/views/home/index.html.erb

and add:

app/views/home/index.html.haml

containing only:

- title 'Subdomain-Authentication'

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

map.root :controller => "home"

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: /

Test the App

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

$ script/server

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

Stop the server with Control-C.

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. Run the generator:

$ script/generate devise_install

which installs a configuration file:

config/initializers/devise.rb

and a localization file.

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 Models, Migrations, and Routes for Users and Admins

This app manages users and administrators separately, allowing the two roles to be implemented differently.

Use Devise to generate models, migrations, and routes for a User and Admin:

$ script/generate devise User
$ script/generate devise Admin

Prevent Logging of Passwords

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

app/controllers/application_controller.rb

to include:

filter_parameter_logging :password, :password_confirmation

Customize the Application

Enable Users and Admins to Have Names

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

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

t.string :name

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

Modify the user and admin models to allow a “name” to be included when adding or updating a record. Modify the files:

app/models/admin.rb
app/models/user.rb

and change:

attr_accessible :email, :password, :password_confirmation

to:

attr_accessible :name, :email, :password, :password_confirmation
validates_uniqueness_of :name, :email, :case_sensitive => false
validates_presence_of :name, :email

to allow users and admins to be created (or edited) with a name attribute. When a user or admin is created, the name and email must be present and must be unique (not used before).

Allow New Admins To Be Created Without Email Confirmation

By default, Devise requires new users or admins to confirm their account by clicking a link in an email message. We’ll leave that in place for users but eliminate the confirmation step for admins.

Modify the file:

app/models/admin.rb

and change:

devise :authenticatable, :confirmable, :recoverable, :rememberable, :trackable, :validatable

to:

devise :authenticatable, :recoverable, :rememberable, :trackable, :validatable

to allow a new administrator to be created without confirming by email.

Use Devise to Register New Users

We could create a controller and views to manage users but Devise provides a built-in “registerable” module that handles this for us. To use it we’ll modify the file app/models/user.rb

and change:

devise :authenticatable, :confirmable, :recoverable, :rememberable, :trackable, :validatable

to:

devise :authenticatable, :confirmable, :recoverable, :registerable, :rememberable, :trackable, :validatable

to provide pages for creating and editing a user.

Note that we could also use the Devise “registerable” module to do the same for admins. However, because we may want to customize behavior for the administrators, we won’t use the Devise “registerable” module.

Create Model and Migration for Subdomains

Each user will be able to register and use a 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:

$ script/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
  validates_uniqueness_of :name, :case_sensitive => false
  validates_presence_of :name
  def to_param
    name
  end
end

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

Enable Users to Own Subdomains

Subdomains belong to users, so we have to set up the User side of the relationship. We’ll also modify the User model so the URL for accessing a user uses a name instead of a number (the friendly_id gem provides this feature):

class User < ActiveRecord::Base
  has_many :subdomains, :dependent => :destroy
  has_friendly_id :name
  # Include default devise modules.
  # Others available are :lockable, :timeoutable and :activatable.
  devise :authenticatable, :confirmable, :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation
end

Create a Site Model

We’ll create a Site model as a subclass of the Subdomain model so that each user can view a site at their subdomain. 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:

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, Subdomains and an Administrator

Create a rake file:

$ touch lib/tasks/setup.rake

with the following code:

namespace :sdauth do

  desc 'set up subdomain-authentication example with default user and administrator'
  task :setup => ['db:drop', 'db:create', 'db:migrate', 'environment'] do
    puts 'SETTING UP NEW USER AND ADMIN LOGINS'
    puts 'DELETING ANY EXISTING USER AND ADMIN RECORDS'
    user = User.create! do |u|
      u.name = 'firstuser'
      u.email = 'firstuser@test.com'
      u.password = 'please'
      u.password_confirmation = 'please'
    end
    user.confirm!
    puts 'New user created: ' << user.name
    user = User.create! do |u|
      u.name = 'otheruser'
      u.email = 'otheruser@test.com'
      u.password = 'please'
      u.password_confirmation = 'please'
    end
    user.confirm!
    puts 'New user created: ' << user.name

    admin = Admin.create! do |u|
      u.name = 'admin'
      u.email = 'admin@test.com'
      u.password = 'please'
      u.password_confirmation = 'please'
    end
    puts 'New admin created: ' << admin.name

    subdomain = Subdomain.create! do |s|
      s.user_id = '1'
      s.name = 'foo'
    end
    puts 'created subdomain: ' << subdomain.name
    subdomain = Subdomain.create! do |s|
      s.user_id = '1'
      s.name = 'bar'
    end
    puts 'created subdomain: ' << subdomain.name
  end
end

Run the rake file:

$ rake sdauth:setup

Implement Subdomains

Configure subdomain_routes

Create a configuration file for the subdomain_routes gem:

$ touch config/initializers/subdomain_routes.rb

The following configuration is suitable if you are developing the app to run at “localhost” and deploying to a domain such as “example.com”. See the information at http://github.com/mholling/subdomain_routes/ if your development or deployment is different.

if Rails.env.to_sym == :development
  require 'subdomain_routes'
    # make sure subdomain_routes can handle a nil subdomain:
    # http://code.matthewhollingworth.net/articles/2009-06-02-adding-subdomains-to-rails-routing
    SubdomainRoutes::Config.domain_length = 1
end
if Rails.env.to_sym == :production
  require 'subdomain_routes'
    # make sure subdomain_routes can handle a nil subdomain:
    # http://code.matthewhollingworth.net/articles/2009-06-02-adding-subdomains-to-rails-routing
    SubdomainRoutes::Config.domain_length = 2
end

Set Up an Administrative Subdomain

Create a Controller and View for an Administrative Home Page

We want administrators to visit the “admin” subdomain and view an administrative home page.

Create a controller for an administrative home page:

$ touch app/controllers/admin_home_controller.rb

with the following code:

class AdminHomeController < ApplicationController
  before_filter :authenticate_admin!
  def index
  end
end

Create a view for an administrative home page:

$ mkdir app/views/admin_home/
$ touch app/views/admin_home/index.html.haml

with the following code:

- title 'Admin Home'
%p
  Welcome to the Admin section of the application.
= link_to "View List of Admins", admin_admins_path

Add the following routes to implement this use case. Edit the file config/routes.rb and add:

map.subdomain :admin, :namespace => nil do |admin|
  admin.root :controller => "adminhome"
end

If you launch the application and visit
http://admin.localhost:3000/
you’ll be prompted to sign in as an administrator.

Sign in as “admin@test.com” with password “admin123”. You’ll see an error page because we haven’t set up routing, controller and views to implement a link to “View List of Admins”.

Create a Controller and Views to Manage Administrators

We want to view a list of administrators when we log in as an administrator at the “admin” subdomain. We also want to allow administrators to edit, delete, and add new administrators.

Create a controller to manage administrators:

$ touch app/controllers/admins_controller.rb

with the following code:

class AdminsController < InheritedResources::Base
  defaults :route_prefix => 'admin'
  actions :index, :show, :new, :edit, :create, :update, :destroy
  respond_to :html, :js, :xml, :json
  before_filter :authenticate_admin!

  protected

    def collection
      paginate_options ||= {}
      paginate_options[:page] ||= (params[:page] || 1)
      paginate_options[:per_page] ||= (params[:per_page] || 20)
      @admins ||= end_of_association_chain.paginate(paginate_options)
    end

end

Create views to manage administrators:

$ mkdir app/views/admins/
$ touch app/views/admins/_form.html.haml
$ touch app/views/admins/_item.html.haml
$ touch app/views/admins/edit.html.haml
$ touch app/views/admins/index.html.haml
$ touch app/views/admins/new.html.haml
$ touch app/views/admins/show.html.haml

Add the following code to each file:

app/views/admins/_form.html.haml

- form.inputs do
  = form.input :name, :label => 'Name'
  = form.input :email, :label => 'Email'
  = form.input :password, :label => 'Password'

app/views/admins/_item.html.haml

- content_tag_for(:tr, admin, :class => cycle(:odd, :even)) do
  %td.name= h admin.try(:name)
  %td.actions
    = link_to 'Show', admin_admin_path(admin)
    |
    = link_to 'Edit', edit_admin_admin_path(admin)
    |
    = link_to 'Destroy', admin_admin_url(admin), :confirm => 'Are you sure?', :method => :delete

app/views/admins/edit.html.haml

%h1.heading
  = "Editing admin %s" % @admin.id

- semantic_form_for(@admin, :url => { :controller => 'admins', :action => 'update' }) do |form|
  = render 'form', :form => form
  - form.buttons do
    = form.commit_button 'Update'

%p.actions
  = link_to 'Cancel', admin_admins_path

app/views/admins/index.html.haml

%h1.heading
  = "Admins"

%p.actions
  = link_to 'New Admin', new_admin_admin_path

%table
  %thead.header
    %tr
      %th.name= 'Name'
      %th.actions= 'Actions'
  %tbody.items.admins
    - @admins.each do |admin|
      = render 'item', :admin => admin

= will_paginate(@admins)

app/views/admins/new.html.haml

%h1.heading
  = 'New Admin'

- semantic_form_for(@admin, :url => { :controller => 'admins', :action => 'create' }) do |form|
  = render 'form', :form => form
  - form.buttons do
    = form.commit_button 'Create'

%p.actions
  = link_to 'Cancel', admin_admins_path

app/views/admins/show.html.haml

%h1.heading
  = "Admin %s" % @admin.id

- content_tag_for(:dl, @admin) do
  %dt.label= 'Name'
  %dd.name= h @admin.try(:name)
  %dt.label= 'Email'
  %dd.name= h @admin.try(:email)

%p.actions
  = link_to 'Edit', edit_admin_admin_path(@admin)
  |
  = link_to 'List of Admins', admin_admins_path

Implement Routing for Administrators

Modify the following routes in the file config/routes.rb to implement this use case:

map.subdomain :admin, :namespace => nil do |admin|
  admin.root :controller => "AdminHome"
  admin.resources :admins
end

Now you can sign in as “admin@test.com” with password “admin123”. You can “View List of Admins” and edit, delete, and add new administrators.

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.

Create views to create and edit users:

$ mkdir app/views/registrations
$ touch app/views/registrations/edit.html.haml
$ touch app/views/registrations/new.html.haml

Add the following code to each file:

app/views/registrations/edit.html.haml

%h1.heading
  = "Editing %s" % @user.name

- semantic_form_for(resource_name, resource, :url => registration_path(resource_name), :html => { :method => :put }) do |form|
  - form.inputs do
    = form.input :name, :label => 'Name'
    = form.input :email, :label => 'Email'
    = form.input :password, :label => 'Password (leave blank if you don\'t want to change it)'
    = form.input :password_confirmation, :label => 'Password confirmation'
    = form.input :current_password, :label => 'Current Password (we need your current password to confirm your changes)'
  - form.buttons do
    = form.commit_button 'Update'

%p.actions
  = link_to 'Delete my account', registration_path(resource_name), :confirm => 'Are you sure?', :method => :delete

%p.actions
  = link_to 'Back', :back

app/views/registrations/new.html.haml

%h1.heading
  = 'Sign up'

- semantic_form_for(resource_name, resource, :url => registration_path(resource_name)) do |form|
  - form.inputs do
    = form.input :name, :label => 'Name'
    = form.input :email, :label => 'Email'
    = form.input :password, :label => 'Password'
    = form.input :password_confirmation, :label => 'Password confirmation'
  - form.buttons do
    = form.commit_button 'Sign up'

Devise’s default behaviour allows any logged-in user to be able 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 link to the home page that shows a list of users. And we want to be able to view a page that shows details about each user.

Note that we do not need controller methods or views to create a new user or edit or delete a user. We use the “registerable” module from Devise to create, edit or delete a user. We already created customized views for these actions in app/views/registrations.

Create a controller to display users:

$ touch app/controllers/users_controller.rb

with the following code:

class UsersController < InheritedResources::Base

  actions :index, :show
  respond_to :html, :js, :xml, :json

  protected

    def collection
      paginate_options ||= {}
      paginate_options[:page] ||= (params[:page] || 1)
      paginate_options[:per_page] ||= (params[:per_page] || 20)
      @users ||= end_of_association_chain.paginate(paginate_options)
    end

end

Create views to display users:

$ mkdir app/views/users/
$ touch app/views/users/_item.html.haml
$ touch app/views/users/index.html.haml
$ touch app/views/users/show.html.haml

Add the following code to each file:

app/views/users/_item.html.haml

- content_tag_for(:tr, user, :class => cycle(:odd, :even)) do
  %td.name=link_to user.name, user_path(user)
  %td.actions
    = link_to 'Edit', edit_user_registration_path
    |
    = link_to 'Destroy', registration_path(:user), :confirm => 'Are you sure?', :method => :delete

app/views/users/index.html.haml

%h1.heading
  = "Users"

%table
  %thead.header
    %tr
      %th.name= 'Name'
      %th.actions= 'Actions'
  %tbody.items.users
    - @users.each do |user|
      = render 'item', :user => user

= will_paginate(@users)

app/views/users/show.html.haml

%h1.heading
  = "User %s" % @user.id

- content_tag_for(:dl, @user) do
  %dt.label= 'Name'
  %dd.name= h @user.try(:name)
  %dt.label= 'Email'
  %dd.name= h @user.try(:email)

%p.actions
  = link_to 'Edit', edit_user_registration_path
  |
  = link_to 'List of Users', users_path

Implement Routing for the Main Domain

Add the following routes in the file config/routes.rb to implement this use case:

map.subdomain nil do |main|
  main.root :controller => "home"
  main.resources :users
end

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.haml

with these changes:

- title 'Subdomain-Authentication'
%p
  This is my new app.
= link_to "View List of Users", users_path

Add Navigation Links to the Application Layout

We want links to sign up, log in, etc. on each page of the application.

Modify the file:

app/views/layouts/application.html.haml

with these changes:

!!! Strict
%html{html_attrs}

  %head
    %title
      = h(yield(:title) || "Untitled")
    %meta{"http-equiv"=>"Content-Type", :content=>"text/html; charset=utf-8"}/
    = stylesheet_link_tag 'application'
    = yield(:head)

  %body
    #container
      #navigation
        - if user_signed_in?
          - if @site
            = link_to @site.name + " home", site_root_path(@site)
          - else
            = link_to "Home", root_path
          |
          = link_to 'My Account', user_path(current_user)
          |
          = link_to 'Sign out', destroy_user_session_path
        - if admin_signed_in?
          = link_to 'Admin Home', admin_root_path
          |
          = link_to 'Sign out', destroy_admin_session_path
        - if !user_signed_in? && !admin_signed_in?
          - if @site
            = link_to @site.name + " home", site_root_path(@site)
          - else
            = link_to "Home", root_path
          |
          = link_to 'Sign Up', new_registration_path(:user)
          |
          = link_to 'Admin Login', admin_root_path
          |
          = link_to 'User Login', new_user_session_path

        - flash.each do |name, msg|
          = content_tag :div, msg, :id => "flash_#{name}"
      #content
        - if show_title?
          %h1=h yield(:title)

        = yield

Test Sign Up

If you launch the application and visit
http://admin.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 Subdomain Sites

Create a Controller and Views to Manage Subdomains

Our use case specifies that each registered user can 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:

$ touch app/controllers/subdomains_controller.rb

with the following code:

class SubdomainsController < InheritedResources::Base
  belongs_to :user

  actions :index, :show, :new, :edit, :create, :update, :destroy
  respond_to :html, :js, :xml, :json
  before_filter :find_user, :only => [:new, :edit, :create, :update, :destroy]

  def create
    create!{ user_url(@user) }
  end

  def destroy
    destroy!{ user_url(@user) }
  end

  protected

    def collection
      paginate_options ||= {}
      paginate_options[:page] ||= (params[:page] || 1)
      paginate_options[:per_page] ||= (params[:per_page] || 20)
      @subdomains ||= end_of_association_chain.paginate(paginate_options)
    end

    def find_user
      @user = User.find(params[:user_id])
      unless current_user == @user
        flash[:alert]  = "You are not allowed to create or change someone else's subdomain."
        redirect_to user_path(@user)
      end
    end

end

Create views to manage subdomains:

$ mkdir app/views/subdomains/
$ touch app/views/subdomains/_form.html.haml
$ touch app/views/subdomains/_item.html.haml
$ touch app/views/subdomains/index.html.haml
$ touch app/views/subdomains/new.html.haml

Add the following code to each file:

app/views/subdomains/_form.html.haml

- form.inputs do
  = form.input :name, :label => 'Name'
  = form.input :user_id, :as => :hidden

app/views/subdomains/_item.html.haml

- content_tag_for(:tr, subdomain, :class => cycle(:odd, :even)) do
  %td.name= h subdomain.try(:name)
  %td.user_id= link_to subdomain.user.name, user_url(subdomain.user)
  %td.actions
    = link_to 'Destroy', subdomain_url(subdomain), :confirm => 'Are you sure?', :method => :delete

app/views/subdomains/index.html.haml

%h1.heading
  = "Subdomains"

%table
  %thead.header
    %tr
      %th.name= 'Name'
      %th.user_id= 'User'
      %th.actions= 'Actions'
  %tbody.items.subdomains
    - @subdomains.each do |subdomain|
      = render 'item', :subdomain => subdomain

= will_paginate(@subdomains)

app/views/subdomains/new.html.haml

%h1.heading
  = 'New Subdomain'

- semantic_form_for [@user, @subdomain]  do |form|
  = render 'form', :form => form
  - form.buttons do
    = form.commit_button 'Create'

%p.actions
  = link_to 'Cancel', user_subdomains_path

Implement Routing for Subdomains

Modify the following routes in the file config/routes.rb:

map.subdomain nil do |main|
  main.root :controller => "home"
  main.resources :users do |users|
    users.resources :subdomains, :except => [:edit, :show], :shallow => true
  end
end

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

Each registered user can 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.” 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 manage sites:

$ touch app/controllers/sites_controller.rb

with the following code:

class SitesController < InheritedResources::Base

  actions :index, :show, :new, :edit, :create, :update, :destroy
  respond_to :html, :js, :xml, :json

  def show
    @site = Site.find_by_name(params[:site_id])
    show!
  end

  protected

    def collection
      paginate_options ||= {}
      paginate_options[:page] ||= (params[:page] || 1)
      paginate_options[:per_page] ||= (params[:per_page] || 20)
      @sites ||= end_of_association_chain.paginate(paginate_options)
    end

end

Create views to display sites:

$ mkdir app/views/sites/
$ touch app/views/sites/_item.html.haml
$ touch app/views/sites/index.html.haml
$ touch app/views/sites/show.html.haml

Add the following code to each file:

app/views/sites/_item.html.haml

- content_tag_for(:tr, site, :class => cycle(:odd, :even)) do
  %td.user_id= link_to site.name, site_site_path(site,site)
  %td.actions
    = link_to site.user.name, user_url(site.user)

app/views/sites/index.html.haml

%h1.heading
  = "Sites"

%table
  %thead.header
    %tr
      %th.actions= 'Site | '
      %th.actions= 'Belongs to'
  %tbody.items.sites
    - @sites.each do |site|
      = render 'item', :site => site

= will_paginate(@sites)

app/views/sites/show.html.haml

%h1.heading
  = "Site %s" % @site.id
%h3.heading
  Subdomain:
  = @site.name
%h3.heading
  Belongs to:
  = link_to @site.user.name, user_path(@site.user)
%p.actions
  = link_to 'List of Sites', site_sites_path(@site)

Implement Routing for Sites

Add the following routes in the file config/routes.rb to implement this use case:

map.subdomain :model => :site, :namespace => nil do |site|
  site.root :controller => "sites", :action => "show"
  site.resources :sites, :only => [:index, :show] 
end

Modify the User Views to Include Subdomains

Add a file to implement a “partial” for subdomains:

$ touch app/views/users/_subdomain_item.html.haml

Add the following code to the file:

- content_tag_for(:tr, subdomain, :class => cycle(:odd, :even)) do
  %td.name= link_to site_root_path(subdomain).to_s, site_root_path(subdomain)
  %td.actions
    = link_to 'Destroy', subdomain_url(subdomain), :confirm => 'Are you sure?', :method => :delete

Edit the file app/views/users/show.html.haml:

%h1.heading
  = "User %s" % @user.id

- content_tag_for(:dl, @user) do
  %dt.label= 'Name'
  %dd.name= h @user.try(:name)

%p.actions
  = link_to 'New Subdomain', new_user_subdomain_path(@user)

%table
  %thead.header
    %tr
      %th.name= 'Subdomains'
      %th.actions= 'Actions'
  %tbody.items.subdomains
    - @user.subdomains.each do |subdomain|
      = render 'subdomain_item', :subdomain => subdomain

%p.actions
  = link_to 'Edit', edit_user_registration_path
  |
  = link_to 'List of Users', users_path

Test Creating Subdomains

If you launch the application and visit
http://localhost:3000/
and login as a user, you can click a link to create a new subdomain.

Deploy to Heroku

Set Up Heroku

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. After you set up a Heroku account, install the Heroku gem:

$ sudo gem install heroku

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.

Create Your Application on Heroku

Use the Heroku create command to create and name your new app:

$ heroku create _myapp_

Heroku Add-ons and DNS Configuration

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 yur 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

Set Up Your Application on Heroku

Push your application to Heroku:

$ git push heroku master

Set up your Heroku database:

$ heroku rake db:migrate

Initialize your application database:

$ heroku rake sdauth:setup

Visit Your Site

Open your Heroku site in your default web browser:

$ heroku open

Troubleshooting

If you get errors, you can troubleshoot by reviewing the log files:

$ heroku logs

Conclusion

This concludes the tutorial for creating a Ruby on Rails web application that uses subdomains and provides user management and authentication.

Credits

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

Clone this wiki locally