Welcome to the third part of Juggernaut Rails Chat Tutorial

Today we will start Ruby code development.

First of all, we need to create user model. He should be able to login and chat. Also we need administrator role. Administrator should be able to create and destroy chat rooms.

Let's use account attribute to determine whether user is an std user or an administrator.

User model

User model is remarkably simple. It contains login, hashed password, salt and last login timestamp.


script/generate model User

Migration file:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :login
      t.string :hashed_password
      t.string :salt
      # Account type - user/admin
      t.string :account

      t.timestamps
    end
  end

  def self.down
    drop_table :users
  end
end

Let's "fill" user:

require 'digest/sha2'

class User < ActiveRecord::Base

  LOGIN_MAX_LENGTH    = 14
  LOGIN_MIN_LENGTH    = 5
  PASSWORD_MAX_LENGTH = 16
  PASSWORD_MIN_LENGTH = 8

  attr_accessor :password_confirmation
  attr_accessor :password
  attr_accessor :current_password

  validates_uniqueness_of :login
  validates_length_of :login, :within =>  LOGIN_MIN_LENGTH..LOGIN_MAX_LENGTH
  validates_format_of :login, :with => /^[A-ZąćęłńóśżźĄĆĘŁŃÓŚŹŻ0-9_]*$/i
  validates_format_of :password, :with => /^[A-ZąćęłńóśżźĄĆĘŁŃÓŚŹŻ0-9_]*$/i
  validates_length_of :password, :within => PASSWORD_MIN_LENGTH..PASSWORD_MAX_LENGTH
  validates_inclusion_of :account, :in => ["admin", "user"]
  validates_confirmation_of :password


  # Find user and try to authenticate him
  def self.authenticate(login, password)
    user = self.find_by_login(login)
    if user
      expected_password = encrypted_password(password, user.salt)
      if user.hashed_password != expected_password
        user = nil
      end
    end
    user
  end

  def password
    @password
  end

  # Generate hashed password
  def password=(pwd)
    @password = pwd
    return if pwd.blank?
    create_new_salt
    self.hashed_password = User.encrypted_password(self.password, self.salt)
  end

  # Log in user
  def login!(session)
    session[:user_id] = self.id
  end

  # Log out user
  def self.logout!(session)
    session[:user_id] = nil
  end

  def clear_password!
    self.password = nil
    self.password_confirmation = nil
  end

  def account_type
    return :user if self.account == 'user'
    return :admin if self.account == 'admin'
  end

  protected

  def create_new_salt
    self.salt = self.object_id.to_s + rand.to_s
  end

  def self.encrypted_password(password, salt)
    return if password.nil? || salt.nil?
    string_to_hash = password + "GB609ru30j)(*&Th" + salt
    Digest::SHA2.hexdigest(string_to_hash)
  end

end

User exists. Now let's init db session store

Database sessions


rake db:sessions:create

(in /home/maciek/Ruby/RailsChat)
 exists  db/migrate
 create  db/migrate/20100522201155_create_sessions.rb

Uncomment in config/environment.rb :


config.action_controller.session_store = :active_record_store

Add into controllers/application.rb:

protect_from_forgery :secret => 'a8310eac6747c66f90127ef3370c5ad7'

DB migrating:


rake db:migrate

Ok, now it's time to create some views but first lets add one administrator via db migration:

Admin


script/generate migration AddAdmin

Migration:

class AddAdmin < ActiveRecord::Migration   
  def self.up
    User.delete_all
    User.create(
      :login => 'admin',
      :password => 'mysuperpassword',
      :account => 'admin')
  end

  def self.down
    User.delete_all
  end
end

Registration

First of all, it is required to create registration controller:


script/generate controller Register index

Main layout

Layout 1

In layouts/ create main.erb file containing:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
  PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xml:lang="en" lang="en" >
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <meta name="language" content="pl"/>
    <meta name="description" content="Racer - Rails Chatter" />
    <%= stylesheet_link_tag 'main' %>
    <title>Racer - Rails Chatter</title>
  </head>
  <body>
    <div id="contener">
      <div id="logo"></div>
      <div id="menu">
        <% !@logged   %>
          <%= link_to "Login", :controller => :main  %>
          <%= link_to "Register", :controller => :register  %>
        <% else %>
          <%= link_to "Logout", :controller => :main, :action => :logout  %>
        <% end %>
      </div>
      <div id="content">
         <% if flash[:error] %>
          <%= "<div id=\"error\">#{flash[:error]}</div>" %>
         <% end %>
         <% if flash[:message] %>
          <%= "<div id=\"message\">#{flash[:message]}</div>" %>
         <% end %>
         <%= yield %>
      </div>
    </div>
  </body>
</html>

And also main.css:

body {
    background: #f8f8f8;
}

#contener {
    margin: 0 auto; width: 900px;
    margin-top: 20px;
}

#logo {
    background: url('/images/racer_logo.png') no-repeat;
    width: 430px; height: 220px; float: left;
    border: 1px solid black;
}

#content {
    width: 100%; float: left; border: 1px solid black;
    margin-top: 50px; background: #ffffff;
    padding: 20px;
    -moz-border-radius: 24px;
    -webkit-border-radius: 24px;
}

#error {
    border: 1px solid red; padding: 5px; margin: 10px 10px 10px 0;
    width: 430px;
}

#message {
    border: 1px solid green; padding: 5px; margin: 10px 10px 10px 0;
    width: 430px;
}

.register_input {
    width: 150px; float: left;
}
.errorExplanation {
    margin: 0 auto; border: 1px solid #DA0211;
    padding: 5px 5px 0 15px; background: #FFBABA; color: #DA0211;
    margin-bottom: 10px;
}
.errorExplanation h2 {
    font-size: 13px; margin-top: 8px;
}
.errorExplanation li {
    font-size: 13px;
    padding-left: 16px;list-style: circle inside;
    padding-bottom: 4px;
 }
.errorExplanation p {
     padding: 10px 0 0 0; font-size: 13px;
 }
.errorExplanation ul {
     padding: 2px 0 2px 0;
 }
.fieldWithErrors input, .fieldWithErrors select {
    border: 1px solid red;
}
#menu {
    float: right; width: 400px; font-size: 12px;
    margin-top: 200px; text-align: right;
}

#menu a {
    text-decoration: none; margin-left: 40px;
}

#room_list td {
    border-right: 1px solid black; padding: 4px 20px;
}

Let's get back to our view in views/register/index.erb:

<h3>Registration</h3>
<% form_for :user do |f| %>

    <%= f.error_messages %>

<span class="register_input">Login:</span> <%= f.text_field :login %><br/>
<span class="register_input">Password:</span> <%= f.password_field :password %><br/>
<span class="register_input">Password confirmation:</span> <%= f.password_field :password_confirmation %><br/><br/>
    <%= f.submit "Register!" %>

<% end %>

And finally register_controller.rb:

class RegisterController < ApplicationController
  layout "main"

  def index
    if request.post?
      @user = User.new(params[:user])
      @user.account = 'user'
      if @user.save
        flash[:message] = "User registered successfully!"
        redirect_to :controller => :main
      else
        @user.clear_password!
      end
    end
  end

end

Registration is ready. Now login panel.

Login panel

Controller:

script/generate controller Main index login logout

config/routes.rb:


map.root :controller => :main

Controller content:

class MainController < ApplicationController
  layout "main"

  def index
    if !session[:user_id].nil?
      @logged = true
      flash[:message] = "You are logged in as: #{User.find(session[:user_id]).login.capitalize}"
    end
  end

  def login
    if request.post?
       @user = User.authenticate(params[:user][:login], params[:user][:password])
       if @user
         @user.login!(session)
         session[:color] = "rgb(#{rand(255).to_s},#{rand(255).to_s},#{rand(255).to_s})"
         redirect_to :controller => @user.account_type == :user ? :chat : :admin
       else
         flash[:error] = "Invalid login or password"
         redirect_to :action => :index
       end
    else
      redirect_to :action => :index
    end
  end

  def logout
    User.logout!(session)
    flash[:message] = "Wylogowano poprawnie"
    redirect_to :action => :index
  end

end

Login view:

<% if @logged %>

  <%= button_to "Go chat", :controller => "chat" %>

<% else %>

<% form_for :user, :url => { :action => "login" } do |f| %>
    <%= f.error_messages %>
    Login: <%= f.text_field :login %><br/>
    Hasło: <%= f.password_field :password %><br/><br/>
    <%= f.submit "Go chat" %>
<% end %>

<% end %>

To protect controllers and actions, we use before_filter:

  def authorize
    unless (@user = User.find_by_id(session[:user_id]))
      User.logout!(session)
      flash[:error] = ('Please log in')
      redirect_to :controller => :main
    end
  end

Now let's protect resources dedicated only for administrators:

  def only_admin
    redirect_to :controller => :main unless @user.account_type == :admin
  end

That's all for now

Tutorial parts:

  1. Juggernaut Rails Chat – part I (what is and how to install Juggernaut)
  2. Juggernaut Rails Chat – part II (design)
  3. Juggernaut Rails Chat – part III (registration and authentication)
  4. Juggernaut Rails Chat – część IV (zarządzanie pokojami)
  5. Juggernaut Rails Chat – część V (łączenie użytkowników z pokojami)
  6. Juggernaut Rails Chat – część VI (odpalamy Juggernaut i nasz chat)
  7. Juggernaut Rails Chat – część VII (emotikonki i dźwięk)