Intro:

This post is a journal for the process of creating a website in Rails. Some degree of familiarity with Ruby and Rails by the reader is assumed. Our objective is to create a website that utilizes authentication and authorization (and also allow authentication from an outside service). It will also utilize nested resources and routes. For this project, we’re going to build a site that can manage events. We want our users to be able to create events that other users can sign up to attend, and tag their own events with tag categories. Some code will be intentionally left out but my full code can be found on my repo, linked at the bottom of this post. Let’s get started!

Getting Started:

First, we create our models and migrations. I use regular old rails generators to create the files with

  rails generate model Event

and fill in the changes needed afterwards.

  class CreateEvents < ActiveRecord::Migration
    def change
      create_table :events do |t|
        t.string :name
        t.string :location
        t.text :description
        t.datetime :start_time
        t.datetime :end_time
        t.string :time_zone
        t.references :user, index: true

        t.timestamps null: false
      end
    end
  end

Continue to generate and write out migrations for the User, Tag and Comment models. Our comment model will also serve as a join table that can have an attribute :content to hold the text for a comment in addition to tying a user_id to an event. We’ll also want a second join table for Events and Tags and a model for EventTag, since Events can have many Tags, and Tags can have many Events.

  class CreateComments < ActiveRecord::Migration
    def change
      create_table :comments do |t|
        t.text :content
        t.references :user, index: true
        t.references :event, index: true

        t.timestamps null: false
      end
    end
  end

  # add another migration file
  class CreateJoinTableEventTags < ActiveRecord::Migration
    def change
      create_table :event_tags do |t|
        t.integer :event_id
        t.integer :tag_id
      end
    end
  end

And also another join table for the users and events tables so we can build out the feature to allow users to ‘attend’ events. We’ll call this join table Schedules.

  class CreateSchedules < ActiveRecord::Migration
    def change
      create_table :schedules do |t|
        t.integer :event_id  # works the same as t.references :event, index: true
        t.integer :user_id

        t.timestamps null: false
      end
    end
  end

Now to add Devise to handle our authentication needs. Include the devise gem in our Gemfile. We’ll also include Pundit and Omniauth while we’re at it to handle roles and outside authentication service. I’ve decided to use GitHub for this site.

  gem 'devise'
  gem 'pundit'
  gem 'omniauth'
  gem 'omniauth-github'
  gem 'dot-env'

Run bundle to install the gems

  bundle install

GitHub omniauth requires that we get GITHUB_KEY and GITHUB_SECRET keys from github for authentication. You can obtain these directly from github by logging in or creating an account and creating a new application. Get the keys from GitHub and create a new a file the site’s root directory called .env (note: this is a file with no name that has an extension of .env). The dot-env gem included above will allow rails to grab the values from the .env file.

  touch .env

Now add the keys to the .env file, replacing <YOUR_KEY> AND <YOUR_SECRET> with your values from GitHub (or whatever provider you chose)

  GITHUB_KEY=<YOUR_KEY>
  GITHUB_SECRET=<YOUR_SECRET>

Then add Devise to Users so that it can do it’s magic

  rails generate devise:install Users

Then to add Omniauth capability to our Users

  rails generate migration AddOmniauthToUsers provider:string uid:string

Add roles to our User model. Normal users will be create by default. Admins can modify everything but will not be able to be created from sign up.

  class AddRolesToUsers < ActiveRecord::Migration
    def change
      add_column :users, :role, :integer, default: 0
    end
  end

We’ll also need to make changes to our config/devise.rb file to include a config line that will allow Omniauth to work with Devise. We add this line to the devise.rb file instead of omniauth config since we are using Devise for authentication.

  # add this line inside the Devise.setup block to allow omniauth with github
  # scope: 'user' will allow our site to use the info in the user hash sent back from github
  Devise.setup do |config|
    config.omniauth :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET'], scope: 'user'
  end

Then we want to include our migrations to create our tables

  rake db:migrate
  # seed our database with fake data if you have some seed data
  rake db:seed

Now to make sure that our routes are setup properly. Remember we want to make use of nested routes for our comments, since a comment belongs to a user. In addition to setting up a Static controller to handle our home page, we’ll also want to create a new controller to handle omniauth callbacks for omniauth sign ins. This controller will handle all the data that is sent back from github (callbacks)

  Rails.application.routes.draw do
    resources :tags
    devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

    resources :users do
      resources :comments
    end
    resources :schedules
    resources :events

    root 'static#home'
  end

Create the omniauth callbacks controller. I’ve decided to create a app/controllers/users folder and stick a newly created omniauth_callbacks_controller.rb which inherits from the Devise’s omniauth controller.

  # app/controllers/users/omniauth_callbacks_controller.rb
  class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

    def github
      # You need to implement the method below in your model (e.g. app/models/user.rb)
      @user = User.from_omniauth(request.env["omniauth.auth"])
      if @user.persisted?
        sign_in_and_redirect @user
        set_flash_message(:notice, :success, :kind => "Github") if is_navigational_format?
      else
        session["devise.github_data"] = request.env["omniauth.auth"]
        redirect_to new_user_session_path, flash: {error: 'There was an error.'}
      end
    end

    # handle uh oh behavior so your site doesn't freak out when authentication fails
    def failure
      redirect_to root_path
    end

end

Modify the User.rb model file to look for or create users from GitHub. You might want also want to add a couple of simple validations along with defining pundit roles with enum and devise options to allow omniauth. The below also has some ActiveRecord association macros needed for interacting with other models.

  class User < ActiveRecord::Base

    devise :database_authenticatable, :registerable,
           :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :omniauth_providers => [:github]

    has_many :schedules
    has_many :events, through: :schedules#, source: :event
    has_many :locations, through: :events

    has_many :comments

    validates :name, presence: true
    validates :email, uniqueness: true


    enum role: [:normal, :moderator, :admin]

    def self.from_omniauth(auth)
      where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
        user.email = auth.info.email
        user.password = Devise.friendly_token[0,20]
        user.name = auth.info.name || user.auth.info.username
      end
    end

  end

Building MVC:

Now to build out the rest of the models, views, and controllers. We start by building out the rest of models with the proper association macros

  class Schedule < ActiveRecord::Base
    belongs_to :user
    belongs_to :event
  end

  class Tag < ActiveRecord::Base
    has_many :event_tags
    has_many :events, through: :event_tags
  end

  class EventTag < ActiveRecord::Base
    belongs_to :event
    belongs_to :tag
  end

  class Comment < ActiveRecord::Base
    belongs_to :user
    belongs_to :event
  end

The event class will have a bit more code to help validate attributes and also make the time objects for the event more readable in our views

  class Event < ActiveRecord::Base
    belongs_to :organizer, :foreign_key => "user_id", :class_name => 'User'
    has_many :schedules
    has_many :users, through: :schedules#, source: :user
    has_many :comments
    has_many :event_tags
    has_many :tags, through: :event_tags
    accepts_nested_attributes_for :tags

    validates_presence_of :description, :name, :location, :start_time, :end_time
    validate :event_cannot_start_in_the_past, :event_cannot_end_before_start_time

    def start_time
      super.in_time_zone(time_zone) if time_zone
    end

    def end_time
      super.in_time_zone(time_zone) if time_zone
    end

    def self.sort_by_start_time
      self.order('start_time')
    end

    def readable_start_time
      self.start_time.strftime("%A, %d %b %Y %l:%M %p")
    end

    def readable_end_time
      self.end_time.strftime("%A, %d %b %Y %l:%M %p")
    end

    def tags_attributes=(tag_attributes)
      tag_attributes.values.each do |tag_attribute|
        tag = Tag.find_or_create_by(tag_attribute) if tag_attribute[:name].present?
        self.tags << tag if tag
      end
    end

    def event_cannot_start_in_the_past
      if self.start_time.present? && self.start_time < DateTime.now
        errors.add(:start_time, "Event cannot start in the past")
      end
    end

    def event_cannot_end_before_start_time
      if self.end_time.present? && self.start_time.present? && self.end_time < self.start_time
        errors.add(:start_time, "Event cannot end before it starts.")
      end
    end
  end

Controllers:

We want anyone who comes to the site to be able to view our site, but only users who are signed in to be able to create and go to events. We can authorize actions by defining policies. We can use Pundit to craft these policies in a app/policies/resource_policy.rb file. Here’s one for our Event.

  class EventPolicy < ApplicationPolicy

    def create?
      user.admin? || user.moderator? || user.normal?
    end

    def update?
      user.admin? || user.moderator? || record.try(:organizer) == user
    end

    def destroy?
      user.admin? || user.moderator? || record.try(:organizer) == user
    end

  end

Now, only users who created the record, in this case our event, can make changes or create events, while moderators and admins who are signed in can do the same to any event. Now our controller actions will need a way to call on these policies. We can do that with authorize

  class EventsController < ApplicationController

    def create
      Time.zone = params[:event][:time_zone]
      @event = current_user.events.build(event_params)
      @event.organizer = current_user
      if authorize @event
        if @event.save
          redirect_to event_path(@event)
        else
          render :new, flash: {error: 'Uh oh'}
        end
      end
    end

    def edit
      @event = Event.find(params[:id])
      @users = @event.users
      @user = current_user
    end

    def update
      @event = Event.find(params[:id])
      Time.zone = @event.time_zone
      if authorize @event
        if @event.update(event_params)
          redirect_to event_path(@event)
        else
          render :edit
        end
      end
    end

    def destroy
      @event = Event.find(params[:id])
      authorize @event
      if @event.delete
        redirect_to events_path, flash: {message: 'Event was succussfelly removed.'}
      else
        render :show
      end
    end

  end

Views:

To finish up this post, I’ll include the code for the event views, which utilizes ‘<%= render @event %>’

<%= render @event %>

<h4><%= 'Attender'.pluralize(@users.count) %> of this Event:</h4>

<p>
  <% @users.map do |attender| %>
    <%= link_to (attender.name || attender.email), user_path(attender), class: "button" %>
  <% end %>
</p>

<p>
  <% if user_signed_in? %>
    <%= form_tag schedules_path, method: 'POST' do %>
      <%= hidden_field_tag :user_id, current_user.id %>
      <%= hidden_field_tag :event_id, @event.id %>
      <%= submit_tag "Go to this event", class: 'button confirm' %>
    <% end %>
    <%=  button_to "Not going anymore", {:controller => :schedules, :action => 'destroy', :id => @event.id}, :method => :delete, class: 'button cancel' unless @users.nil? %>
  <% end %>
</p>
</div>

<%= render 'comments/comments' %>
<%= render 'tags/tags' %>

When <%= render @events %> is called, Rails will automagically look in the app/views/events folder for any _event.hmtl.erb file. It will pass each element in the @events array and perform the code in the block in the file, whether it’s a single event in a #show action or many events in #index action. app/views/events/_event.html.erb

<div class="right-info">
  <h4>
    <strong><%= link_to event.name, event_path(event) %> by <%= link_to event.organizer.name, user_path(event.organizer) %></strong>
  </h4>
  <p>
    <strong>Location:</strong> <%= event.location %>
  </p>
  <p>
    <strong>Begins:</strong> <%= event.readable_start_time %>
  </p>
  <p>
    <strong>Ends:</strong> <%= event.readable_end_time %>
  </p>
  <p>
    <strong>Description:</strong> <%= event.description %>
  </p>
  <p>
    <%= link_to 'Edit this event', edit_event_path(event), class: 'button' %>
  </p>
</div>

Conclusion:

This has been a quick guide to creating a Rails app with authentication and authorization. This post is by no means extensive or complete, but you can find my full repo on github.