-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorial (Walkthrough)
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
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.
- Administrators can visit the “admin” subdomain and view an administrative home page or list of administrators.
- Only an administrator can create a new administrator (no email confirmation is needed).
- Any administrator can change or delete another administrator.
- Visitors to the main application (without a subdomain) can register as users and create subdomains.
- Any visitor can visit a subdomain and see a “site” home page.
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.
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?).
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
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.
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.
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
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'
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
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 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
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
.
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 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"
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: /
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.
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"
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
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
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).
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.
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.
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.
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
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
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
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 = '[email protected]' 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 = '[email protected]' 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 = '[email protected]' 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
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
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 “[email protected]” 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”.
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
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 “[email protected]” with password “admin123”. You can “View List of Admins” and edit, delete, and add new administrators.
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.
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
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
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
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
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.
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
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'
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)
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
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
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.
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.
Use the Heroku create command to create and name your new app:
$ heroku create _myapp_
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
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
Open your Heroku site in your default web browser:
$ heroku open
If you get errors, you can troubleshoot by reviewing the log files:
$ heroku logs
This concludes the tutorial for creating a Ruby on Rails web application that uses subdomains and provides user management and authentication.
Daniel Kehoe (http://danielkehoe.com/) implemented the application and wrote the tutorial.