NetBeansで作成中のRailsプロジェクトにRestful Authenticationを追加する(その2)

はじめに_

Restful Authentication with all the bells and whistles (new 9/05/08)に従い、NetBeansで開発中のRailsプロジェクトにRestful Authenticationを付け加えるときのメモ。

このページでは、上記のチュートリアルと以下の点が違う。

  • Roomというモデルがあり、ユーザーの役割はRoomごとに異なる

今回の前提_

実施環境は以下のとおり。

  • Windows XP SP2
  • NetBeans 6.7.1
  • Ruby 1.8.6 p287
  • Rails 2.3.5

公開環境は以下のとおり

  • Apache 2.2.14 (起動コマンド:/etc/init.d/apache)
  • Postfix 2.6.5 (/usr/sbin/sendmail)
  • passenger 2.2.7
  • Ruby 1.8.7
  • gem 1.3.5
  • Rails 2.3.5
  • SQLite3 3.6.21

認証モデル_

Roomごとに役割が異なる。つまり、ユーザーはシステム上で複数の役割を持つ。

  • Room:作業の一単位
  • Role:役割として administrator, user
  • User:ユーザー
  • Session:セッション管理用
  • Permission:Room, Role, Userの対応テーブル

Restful Authenticationのインストール_

  • NetBeansでプロジェクトが読み込まれているとする。プロジェクト名の上にカーソルを合わせて右クリック→「Railsプラグイン」を選ぶ。
  • 開いたウィンドウで「リポジトリ」タブをクリック、その後「URLを追加」をクリックし、「http://svn.techno-weenie.net/projects/plugins」を加える。
  • 「新しいプラグイン」タブをクリックし、検索ウィンドウで「restful_authentication」を検索する。
  • restful_authenticationが見つかったら、選択し、インストールする(Subversionは使わなくても良い)

UserモデルとSessionモデルの生成_

restful_authenticationプラグインで使うファイルを生成する。

  • NetBeansでプロジェクトが読み込まれているとする。プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
  • ジェネレーターは「authenticated」を選ぶ
  • 引数は「user sessions --include-activation」を選ぶ

上記コマンドでセッション用のコントローラーとビューができているはず。その後、application_controller.rbに"include AuthenticatedSystem"が付け加えられているかをチェックする。チェックすると付け加えられていないので付け加える。

次にconfig/initalizersにmail.rbを作成する。localhostのsendmailを使う場合は、以下の内容にする。

  • 「構成」をクリック
  • 「initalizers」にカーソルを合わせて「新規作成」→「Rubyファイル」
  • ファイル名は「mail.rb」にする。

mail.rbの中身(Sendmailバージョン)

# Email settings
ActionMailer::Base.delivery_method = :sendmail 
ActionMailer::Base.sendmail_settings = {
  :location => "/usr/sbin/sendmail -t"
}

次に、config/environment.rbの"Rails::Initializer.run do |config|"の後ろに以下の行を追加する。そうすると、このアプリケーションは、レジストレーションやアクティベーションなどの後にユーザーにメールを送るようになる(user_observer.rbとuser_mailer.rbはrestfulauthenticationによって、app/models以下に作られている)。

config.active_record.observers = :user_observer

次に、config/environment.rbに定数を定義するか、config/environments/development.rb(開発用設定ファイル)かconfig/environments/production.rb(公開用設定ファイル)にアプリケーション用の定数を定義する。

どのファイルに記載するかを以下の視点から決定する。

  • 開発環境でも実行環境でも同じ定数を使う→config/environment.rb
  • 開発環境のみでその定数を使う→config/environments/production.rb
  • 実行環境のみでその定数を使う→config/environments/production.rb

具体的には以下を追加する。

## For my application
RootURL = 'http://localhost:3000/'
MyMailAddress = 'mail@yourapplication.com'

そして、user_mailer.rbを以下のように設定する。

user_mailer.rb:

class UserMailer < ActionMailer::Base
  def signup_notification(user)
    setup_email(user)
    @subject    += 'Please activate your new account'  
    @body[:url]  = "#{RootURL}activate/#{user.activation_code}"  
  end
  
  def activation(user)
    setup_email(user)
    @subject    += 'Your account has been activated!'
    @body[:url]  = "#{RootURL}"
  end
  
  def forgot_password(user)
    setup_email(user)
    @subject    += 'You have requested to change your password'
    @body[:url]  = "#{RootURL}reset_password/#{user.password_reset_code}" 
  end

  def reset_password(user)
    setup_email(user)
    @subject    += 'Your password has been reset.'
  end
 
  protected
    def setup_email(user)
      @recipients  = "#{user.email}"
      @from        = "#{MyMailAddress}"
      @subject     = "YourApplication - "
      @sent_on     = Time.now
      @body[:user] = user
    end
end

config以下のファイルを書き換えた場合は、WebRickを再起動すること。

そして、user_observer.rbにパスワードのリセットとパスワード忘れ機能を組み込む。

user_observer.rb:

class UserObserver < ActiveRecord::Observer
  def after_create(user)
    UserMailer.deliver_signup_notification(user)
  end

  def after_save(user) 
    UserMailer.deliver_activation(user) if user.pending?
    UserMailer.deliver_forgot_password(user) if user.recently_forgot_password?
    UserMailer.deliver_reset_password(user) if user.recently_reset_password?
  end

end

room、rolesとpermissionsの設定_

モデルroom、roleとpermissionを設定する。

  • roomの生成
    • プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
    • ジェネレーターは scaffold
    • モデル名は Role
    • 属性ペアは roomname:string
  • roleの生成
    • プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
    • ジェネレーターは scaffold
    • モデル名は Role
    • 属性ペアは rolename:string
  • permissonの生成
    • プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
    • ジェネレーターは model
    • 引数は Permission

db/migrate以下にあるXXX_create_roles.rbを編集する。

  • 「データベースマイグレーション」をクリック
  • 「migrate」をクリック。
  • XXX_create_roles.rbを以下のように編集。
class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table :roles do |t|
      t.string :rolename

      t.timestamps
    end
  end

  def self.down
    drop_table :roles
  end
end

db/migrate以下にあるXXX_create_permissions.rbを編集する。

  • 「データベースマイグレーション」をクリック
  • 「migrate」をクリック。
  • XXX_create_permissions.rbを以下のように編集。
class CreatePermissions < ActiveRecord::Migration
  def self.up
    create_table :permissions do |t|
      t.integer :room_id, :role_id, :user_id, :null => false
      t.timestamps
    end
  end

  def self.down
      drop_table :permissions 
  end
end

db/migrate以下にあるXXX_create_users.rbを編集する。

  • 「データベースマイグレーション」をクリック
  • 「migrate」をクリック。
  • XXX_create_users.rbを以下のように編集。
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table "users", :force => true do |t|
      t.column :login,                     :string
      t.column :email,                     :string
      t.column :crypted_password,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :created_at,                :datetime
      t.column :updated_at,                :datetime
      t.column :remember_token,            :string
      t.column :remember_token_expires_at, :datetime
      t.column :activation_code, :string, :limit => 40
      t.column :activated_at, :datetime
      t.column :password_reset_code, :string, :limit => 40
      t.column :enabled, :boolean, :default => true      
    end
  end

  def self.down
    drop_table "users"
  end
end

元のチュートリアルでは、migrationファイルの中で初期データをセットしていたが、Rails 2.3.4からそれをやめるようになったらしい(情報元:ひげろぐ:Rails 2.3.4で追加されたseeds.rbについて

また、元のチュートリアルでは、システムの管理者をpermissionsで管理していたが、今回のシステムでは、その方法ではシステム管理者を登録できない。そこで、管理者専用ユーザーを用意し、そのユーザーをシステム管理者とすることとする。公開前にシステム管理者のパスワードを変更しておくこと。

db/seeds.rbを以下のように書く。

Role.create(:rolename => 'administrator')
Role.create(:rolename => 'user')

user = User.new
user.login = "ServerAdmin"
user.email = "yourmail@hogehoge"
user.password = "admin"
user.password_confirmation = "admin"
user.save(false)
user.send(:activate!)

次に、app/models以下のファイルをいくつか変更しなければならない。 はじめに、rolesとusersの間の関係を定義する。rolesとusersは多対多の関係なので、has_many :through宣言で関係を示す。

  • 「モデル」をクリック
  • room.rbを以下のように編集。
class Room < ActiveRecord::Base
  has_many :permissions
  has_many :rooms, :through => :permissions
  has_many :roles, :through => :permissions
end
  • 「モデル」をクリック
  • role.rbを以下のように編集。
class Role < ActiveRecord::Base
  has_many :permissions
  has_many :users, :through => :permissions
  has_many :rooms, :through => :permissions
end
  • 「モデル」をクリック
  • permission.rbを以下のように編集。
class Permission < ActiveRecord::Base
  belongs_to :user
  belongs_to :role
  belongs_to :room
end

続いて、以下の変更をuser.rbに行なう。

  • メールアドレスの長さチェックに関する変更
  • userとrolesの間の関係
  • あるユーザーがある役割をもっているかどうかを調べるチェック
  • パスワード忘れに関する記述
  • ユーザーアクティベートに関するいくつかの変更。
  • 「モデル」をクリック
  • user.rbを以下のように編集。
require 'digest/sha1'
class User < ActiveRecord::Base
  # Virtual attribute for the unencrypted password
  attr_accessor :password  

  validates_presence_of     :login, :email
  validates_presence_of     :password,                   :if => :password_required?
  validates_presence_of     :password_confirmation,      :if => :password_required?
  validates_length_of       :password, :within => 4..40, :if => :password_required?
  validates_confirmation_of :password,                   :if => :password_required?
  validates_length_of       :login,    :within => 3..40
  validates_length_of       :email,    :within => 6..100
  validates_uniqueness_of   :login, :email, :case_sensitive => false
  validates_format_of       :email, :with => /(^([^@\s]+)@((?:[-_a-z0-9]+\.)+[a-z]{2,})$)|(^$)/i

  has_many :permissions
  has_many :roles, :through => :permissions
 has_many :rooms, :through => :permissions

  before_save :encrypt_password
  before_create :make_activation_code
 
  # prevents a user from submitting a crafted form that bypasses activation
  # anything else you want your user to change should be added here.
  attr_accessible :login, :email, :password, :password_confirmation

  class ActivationCodeNotFound < StandardError; end
  class AlreadyActivated < StandardError
    attr_reader :user, :message;
    def initialize(user, message=nil)
      @message, @user = message, user
    end
  end
 
  # Finds the user with the corresponding activation code, activates their account and returns the user.
  #
  # Raises:
  #  +User::ActivationCodeNotFound+ if there is no user with the corresponding activation code
  #  +User::AlreadyActivated+ if the user with the corresponding activation code has already activated their account
  def self.find_and_activate!(activation_code)
    raise ArgumentError if activation_code.nil?
    user = find_by_activation_code(activation_code)
    raise ActivationCodeNotFound if !user
    raise AlreadyActivated.new(user) if user.active?
    user.send(:activate!)
    user
  end

  def active?
    # the presence of an activation date means they have activated
    !activated_at.nil?
  end

  # Returns true if the user has just been activated.
  def pending?
    @activated
  end

  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
  # Updated 2/20/08
  def self.authenticate(login, password)   
    u = find :first, :conditions => ['login = ?', login] # need to get the salt
    u && u.authenticated?(password) ? u : nil 
  end

  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
  end

  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end

  def authenticated?(password)
    crypted_password == encrypt(password)
  end

  def remember_token?
    remember_token_expires_at && Time.now.utc < remember_token_expires_at
  end

  # These create and unset the fields required for remembering users between browser closes
  def remember_me
    remember_me_for 2.weeks
  end

  def remember_me_for(time)
    remember_me_until time.from_now.utc
  end

  def remember_me_until(time)
    self.remember_token_expires_at = time
    self.remember_token            = encrypt("#{email}--#{remember_token_expires_at}")
    save(false)
  end

  def forget_me
    self.remember_token_expires_at = nil
    self.remember_token            = nil
    save(false)
  end
 
  def forgot_password
    @forgotten_password = true
    self.make_password_reset_code
  end

  def reset_password
    # First update the password_reset_code before setting the
    # reset_password flag to avoid duplicate email notifications.
    update_attribute(:password_reset_code, nil)
    @reset_password = true
  end 

  #used in user_observer
  def recently_forgot_password?
    @forgotten_password
  end

  def recently_reset_password?
    @reset_password
  end
 
  def self.find_for_forget(email)
    find :first, :conditions => ['email = ? and activated_at IS NOT NULL', email]
  end
 
  def has_role?(rolename)
    self.roles.find_by_rolename(rolename) ? true : false
  end

  def is_server_admin?
    self.login == 'ServerAdmin'
  end

  protected
 
  # before filter
  def encrypt_password
    return if password.blank?
    self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
    self.crypted_password = encrypt(password)
  end
     
  def password_required?
    crypted_password.blank? || !password.blank?
  end
   
  def make_activation_code
    self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
  end
   
  def make_password_reset_code
    self.password_reset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
  end
   

  private
 
  def activate!
    @activated = true
    self.update_attribute(:activated_at, Time.now.utc)
  end   
       
end

lib以下にあるauthenticated_system.rbの変更をする。

  • 「ライブラリー」をクリック
  • authenticated_system.rbにnot_logged_in_required, check_role, check_administrator_roleと permission_denied を以下のように付け加える。
module AuthenticatedSystem
  protected
    # Returns true or false if the user is logged in.
    # Preloads @current_user with the user model if they're logged in.
    def logged_in?
      current_user != :false
    end

    # Accesses the current user from the session.  Set it to :false if login fails
    # so that future calls do not hit the database.
    def current_user
      @current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie || :false)
    end

    # Store the given user id in the session.
    def current_user=(new_user)
      session[:user_id] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
      @current_user = new_user || :false
    end

    # Check if the user is authorized
    #
    # Override this method in your controllers if you want to restrict access
    # to only a few actions or if you want to check if the user
    # has the correct rights.
    #
    # Example:
    #
    #  # only allow nonbobs
    #  def authorized?
    #    current_user.login != "bob"
    #  end
    def authorized?
      logged_in?
    end

    # Filter method to enforce a login requirement.
    #
    # To require logins for all actions, use this in your controllers:
    #
    #   before_filter :login_required
    #
    # To require logins for specific actions, use this in your controllers:
    #
    #   before_filter :login_required, :only => [ :edit, :update ]
    #
    # To skip this in a subclassed controller:
    #
    #   skip_before_filter :login_required
    #
    def login_required
      authorized? || access_denied
    end
   
    def not_logged_in_required
      !logged_in? || permission_denied
    end
   
    def check_role(role)
      unless logged_in? && @current_user.has_role?(role)
        if logged_in?
          permission_denied
        else
          store_referer
          access_denied
        end
      end
    end

    # For server administrator
    def check_server_administrator_role
      logged_in? && (current_user.login == 'ServerAdmin')
    end

    # Redirect as appropriate when an access request fails.
    #
    # The default action is to redirect to the login screen.
    #
    # Override this method in your controllers if you want to have special
    # behavior in case the user is not authorized
    # to access the requested action.  For example, a popup window might
    # simply close itself.
    def access_denied
      respond_to do |format|
        format.html do
          store_location
          flash[:error] = "You must be logged in to access this feature."
          redirect_to :controller => '/session', :action => 'new'
        end
        format.xml do
          request_http_basic_authentication 'Web Password'
        end
      end
    end
   
    def permission_denied     
      respond_to do |format|
        format.html do
          #Put your domain name here ex. http://www.example.com
          domain_name = "http://localhost:3000"
          http_referer = session[:refer_to]
          if http_referer.nil?
            store_referer
            http_referer = ( session[:refer_to] || domain_name )
          end
          flash[:error] = "You don't have permission to complete that action."
          #The [0..20] represents the 21 characters in http://localhost:3000
          #You have to set that to the number of characters in your domain name
          if http_referer[0..20] != domain_name   
            session[:refer_to] = nil
            redirect_to root_path
          else
            redirect_to_referer_or_default(root_path)   
          end
        end
        format.xml do
          headers["Status"]           = "Unauthorized"
          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
          render :text => "You don't have permission to complete this action.", :status => '401 Unauthorized'
        end
      end
    end

    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      session[:return_to] = request.request_uri
    end

    def store_referer
      session[:refer_to] = request.env["HTTP_REFERER"]
    end

    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end

    def redirect_to_referer_or_default(default)
      redirect_to(session[:refer_to] || default)
      session[:refer_to] = nil
    end

    # Inclusion hook to make #current_user and #logged_in?
    # available as ActionView helper methods.
    def self.included(base)
      base.send :helper_method, :current_user, :logged_in?
    end

    # Called from #current_user.  First attempt to login by the user id stored in the session.
    def login_from_session
      self.current_user = User.find(session[:user_id]) if session[:user_id]
    end

    # Called from #current_user.  Now, attempt to login by basic authentication information.
    def login_from_basic_auth
      authenticate_with_http_basic do |username, password|
        self.current_user = User.authenticate(username, password)
      end
    end

    # Called from #current_user.  Finaly, attempt to login by an expiring token in the cookie.
    def login_from_cookie
      user = cookies[:auth_token] && User.find_by_remember_token(cookies[:auth_token])
      if user && user.remember_token?
        user.remember_me
        cookies[:auth_token] = { :value => user.remember_token, :expires => user.remember_token_expires_at }
        self.current_user = user
      end
    end
end

コントローラーの編集_

続いてcontrollerを編集する。元のチュートリアルでは、restful_authenticationによって生成された多くのactionをそれら自身のコントローラーに移している。目的は、restfulの振る舞いにできる限りそうようにするため。また、将来の拡張しやすいようにしておくため。

まず、users_controller.rbを編集する。

  • 「コントローラ」をクリック
  • users_controller.rbを以下のように編集。
class UsersController < ApplicationController
  layout 'application'
  before_filter :not_logged_in_required, :only => [:new, :create]
  before_filter :login_required, :only => [:show, :edit, :update]
  before_filter :check_server_administrator_role, :only => [:index, :enable]

  def index
      @users = User.find(:all)
      @numOfUserRooms = Permission.count(:room_id, :group => 'user_id')
  end

  #This show action only allows users to view their own profile
  def show
      @user = current_user
      @roles = Role.find(:all)
      @myRooms = Array.new
      @permissions = Permission.find(:all, :conditions => ["user_id = ?", @user.id])
      @permissions.each do | permission |
        @myRooms.push(permission.room)
      end
  end

  # render new.rhtml
  def new
    @user = User.new
  end

  def create
    cookies.delete :auth_token
    @user = User.new(params[:user])
    @user.save!
    #Uncomment to have the user logged in after creating an account - Not Recommended
    #self.current_user = @user
    flash[:notice] = "Thanks for signing up! Please check your email to activate your account before logging in."
    redirect_to login_path
  rescue ActiveRecord::RecordInvalid
    flash[:error] = "There was a problem creating your account."
    render :action => 'new'
  end

  def edit
    @user = current_user
  end

  def update
    @user = User.find(current_user)
    if @user.update_attributes(params[:user])
      flash[:notice] = "User updated"
      redirect_to :action => 'show', :id => current_user
    else
      render :action => 'edit'
    end
  end

  def destroy
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, false)
      flash[:notice] = "User disabled"
    else
      flash[:error] = "There was a problem disabling this user."
    end
    redirect_to :action => 'index'
  end

  def enable
    @user = User.find(params[:id])
    if @user.update_attribute(:enabled, true)
      flash[:notice] = "User enabled"
    else
      flash[:error] = "There was a problem enabling this user."
    end
      redirect_to :action => 'index'
  end

end

次にsessions_controller.rbを編集する。

  • 「コントローラ」をクリック
  • sessions_controller.rbを以下のように編集。
# This controller handles the login/logout function of the site.  
class SessionsController < ApplicationController
  layout 'application'
  before_filter :login_required, :only => :destroy
  before_filter :not_logged_in_required, :only => [:new, :create]
  
  # render new.rhtml
  def new
  end

  def create
    password_authentication(params[:login], params[:password])
  end

  def destroy
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    reset_session
    flash[:notice] = "You have been logged out."
    redirect_to login_path   
  end
 
  protected
 
  # Updated 2/20/08
  def password_authentication(login, password)
    user = User.authenticate(login, password)
    if user == nil
      failed_login("Your username or password is incorrect.")
    elsif user.activated_at.blank?   
      failed_login("Your account is not active, please check your email for the activation code.")
    elsif user.enabled == false
      failed_login("Your account has been disabled.")
    else
      self.current_user = user
      successful_login
    end
  end
 
  private
 
  def failed_login(message)
    flash.now[:error] = message
    render :action => 'new'
  end
 
  def successful_login
    if params[:remember_me] == "1"
      self.current_user.remember_me
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
    end
      flash[:notice] = "Logged in successfully"
      return_to = session[:return_to]
      if return_to.nil?
        redirect_to user_path(self.current_user)
      else
          redirect_to return_to
      end
  end

end

次にさらに二つのcontrollerを生成する。

  • Passwordsの生成
    • プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
    • ジェネレーターは controller
    • モデル名は Passwords
    • ビューは、edit new
  • Accountsの生成
    • プロジェクト名の上にカーソルを合わせて右クリック→「生成」を選ぶ。
    • ジェネレーターは controller
    • モデル名は Accounts
    • ビューは、edit

passwords_controller.rbを編集する。

class PasswordsController < ApplicationController
  layout 'application'
  before_filter :not_logged_in_required, :only => [:new, :create]
  
  # Enter email address to recover password    
  def new
  end
    
  # Forgot password action
  def create    
    return unless request.post?
    if @user = User.find_for_forget(params[:email])
      @user.forgot_password
      @user.save      
      flash[:notice] = "A password reset link has been sent to your email address." 
      redirect_to login_path
    else
      flash[:notice] = "Could not find a user with that email address." 
      render :action => 'new'
    end        
  end
  
  # Action triggered by clicking on the /reset_password/:id link recieved via email
  # Makes sure the id code is included
  # Checks that the id code matches a user in the database
  # Then if everything checks out, shows the password reset fields
  def edit
    if params[:id].nil? 
      render :action => 'new'
      return
    end
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
  rescue
    logger.error "Invalid Reset Code entered." 
    flash[:notice] = "Sorry - That is an invalid password reset code. Please check your code and try again. (Perhaps your email client inserted a carriage return?)" 
    #redirect_back_or_default('/')
    redirect_to new_user_path
  end
    
  # Reset password action /reset_password/:id
  # Checks once again that an id is included and makes sure that the password field isn't blank
  def update
    if params[:id].nil? 
      render :action => 'new'
      return
    end
    if params[:password].blank? 
      flash[:notice] = "Password field cannot be blank."
      render :action => 'edit', :id => params[:id]
      return
    end
    @user = User.find_by_password_reset_code(params[:id]) if params[:id]
    raise if @user.nil?
    return if @user unless params[:password]
      if (params[:password] == params[:password_confirmation])
        #Uncomment and comment lines with @user to have the user logged in after reset - not recommended
        #self.current_user = @user #for the next two lines to work
        #current_user.password_confirmation = params[:password_confirmation]
        #current_user.password = params[:password]
        #@user.reset_password
        #flash[:notice] = current_user.save ? "Password reset" : "Password not reset" 
        @user.password_confirmation = params[:password_confirmation]
        @user.password = params[:password]
        @user.reset_password        
        flash[:notice] = @user.save ? "Password reset." : "Password not reset."
      else
        flash[:notice] = "Password mismatch." 
        render :action => 'edit', :id => params[:id]
        return
      end  
      redirect_to login_path 
  rescue
    logger.error "Invalid Reset Code entered" 
    flash[:notice] = "Sorry - That is an invalid password reset code. Please check your code and try again. (Perhaps your email client inserted a carriage return?)" 
    redirect_to new_user_path
  end
    
end

accounts_controller.rbを編集。

class AccountsController < ApplicationController
  layout 'application'
  before_filter :login_required, :except => :show
  before_filter :not_logged_in_required, :only => :show

  # Activate action
  def show
    # Uncomment and change paths to have user logged in after activation - not recommended
    #self.current_user = User.find_and_activate!(params[:id])
    User.find_and_activate!(params[:id])
    flash[:notice] = "Your account has been activated! You can now login."
    redirect_to login_path
  rescue User::ArgumentError
    flash[:notice] = 'Activation code not found. Please try creating a new account.'
    redirect_to new_user_path   
  rescue User::ActivationCodeNotFound
    flash[:notice] = 'Activation code not found. Please try creating a new account.'
    redirect_to new_user_path
  rescue User::AlreadyActivated
    flash[:notice] = 'Your account has already been activated. You can log in below.'
    redirect_to login_path
  end
   
  def edit
  end
 
  # Change password action 
  def update
    return unless request.post?
    if User.authenticate(current_user.login, params[:old_password])
      if ((params[:password] == params[:password_confirmation]) && !params[:password_confirmation].blank?)
        current_user.password_confirmation = params[:password_confirmation]
        current_user.password = params[:password]       
        if current_user.save
          flash[:notice] = "Password successfully updated."
          redirect_to root_path #profile_url(current_user.login)
        else
          flash[:error] = "An error occured, your password was not changed."
          render :action => 'edit'
        end
      else
        flash[:error] = "New password does not match the password confirmation."
        @old_password = params[:old_password]
        render :action => 'edit'     
      end
    else
      flash[:error] = "Your old password is incorrect."
      render :action => 'edit'
    end   
  end
 
end

roles_controller.rbを編集。

class RolesController < ApplicationController
  layout 'application'
  before_filter :check_server_administrator_role

  def index
    @user = User.find(params[:user_id])
    @all_roles = Role.find(:all)
  end

  def update
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    unless @user.has_role?(@role.rolename)
      @user.roles << @role
    end
    redirect_to :action => 'index'
  end
 
  def destroy
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    if @user.has_role?(@role.rolename)
      @user.roles.delete(@role)
    end
    redirect_to :action => 'index'
  end

end

rooms_controller.rbを編集。

class RoomsController < ApplicationController
  layout 'application'
  before_filter :login_required

  # GET /rooms/1
  # GET /rooms/1.xml
  def show
    permission = check_permission(params[:id])

    unless permission
      flash[:notice] = 'You cannot access the room!'
      redirect_to user_path(current_user)
    else
      @room = permission.room
      respond_to do |format|
        format.html # show.html.erb
        format.xml  { render :xml => @room }
      end
    end
  end

  # GET /rooms/new
  # GET /rooms/new.xml
  def new
    @room = Room.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @room }
    end
  end

  # GET /rooms/1/edit
  def edit
    permission = check_permission(params[:id])

    unless permission
      flash[:notice] = 'You cannot access the room!'
      redirect_to user_path(current_user)
    else
      @room = permission.room
    end
  end

  # POST /rooms
  # POST /rooms.xml
  def create
    @room = Room.new(params[:room])

    respond_to do |format|
      if @room.save
        @permission = Permission.new()
        @permission.user_id = current_user.id
        @permission.room_id = @room.id
        role = Role.find(:first, :conditions => ["rolename = ?", 'administrator'])
        @permission.role_id = role.id
        unless @permission.save
          flash[:notice] = 'A trouble occurs!'
          format.html { render :action => "new" }
          format.xml  { render :xml => @room.errors, :status => :unprocessable_entity }
        end
        flash[:notice] = 'Room was successfully created.'
        format.html { redirect_to(@room) }
        format.xml  { render :xml => @room, :status => :created, :location => @room }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @room.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /rooms/1
  # PUT /rooms/1.xml
  def update
    permission = check_permission(params[:id])
    
    unless permission
      flash[:notice] = 'You cannot access the room!'
      redirect_to user_path(current_user)
    else
      @room = permission.room
      respond_to do |format|
        if @room.update_attributes(params[:room])
          flash[:notice] = 'Room was successfully updated.'
          format.html { redirect_to(@room) }
          format.xml  { head :ok }
        else
          format.html { render :action => "edit" }
          format.xml  { render :xml => @room.errors, :status => :unprocessable_entity }
        end
      end
    end
  end

  # DELETE /rooms/1
  # DELETE /rooms/1.xml
  def destroy
    permission = check_permission(params[:id])
    unless permission
      flash[:notice] = 'You cannot access the room!'
      redirect_to user_path(current_user)
    else
       room = permission.room
       flash[:notice] = "Room #{room.roomname} was successfully deleted."
       room.destroy
       permission.destroy
      respond_to do |format|
        format.html { redirect_to(user_path(current_user)) }
        format.xml  { head :ok }
      end
    end
   
  end

  private
  def check_permission(room_id)
      @user = current_user
      permission = Permission.find(:first, :conditions => ["room_id = ? and user_id = ?", room_id,@user.id ])
      if permission.nil?
       false
      else
        permission
      end
  end
end

ビューの編集_

次はビューの編集。まず、最初にこのアプリケーションのレイアウトを作成する。このレイアウトは少なくとも以下を含むこと。

  • 「ビュー」をクリック
  • 「layout」をクリック
  • application.html.erbを編集。以下を加えると、ログイン時にユーザー名が表示され、個人情報の変更(Edit Profile)、パスワードの変更(Change Password)、ログアウト(Log Out)のリンクが表示される。ログインしており、かつ、administratorならば、その旨が表示される。未ログインならば、ログイン(Log In)、アカウント作成(Sign Up)、パスワード忘れ処理(Forgot Password)が表示される。
<% if logged_in? -%>
<p>Current User: <%=h current_user.login -%> <%=link_to "[#{current_user.login}'s page]", user_path(self.current_user)-%>, <%= link_to "[logout]", logout_path -%></p>
<% else -%>
<p>Not Logged in <%= link_to "[login]", login_path -%> or <%= link_to "[signup]", signup_path -%></p>
<% end -%>

app/views/passwords/edit.html.erbの編集。

<h2>パスワードの変更</h2>
<% form_tag url_for(:action => "update", :id => params[:id]) do %>
    Password:<br />
    <%= password_field_tag :password %><br />
    Confirm Password:<br />
    <%= password_field_tag :password_confirmation %><br />
    <%= submit_tag "Reset Your Password" %>
<% end %>

app/views/passwords/new.html.erbの編集。

<h2>パスワードの再送付</h2>
<% form_tag url_for(:action => 'create') do %>
    What is the email address used to create your account?<br />
    <%= text_field_tag :email, "", :size => 50 %><br />
    <%= submit_tag 'Reset Password' %>
<% end %>

app/views/accounts/edit.html.erbの編集。

<% form_tag url_for(:action => "update") do %>
  <p><label for="old_password" class="block">Old Password</label><br />
  <%= password_field_tag 'old_password', @old_password, :size => 45 %></p>

  <p><label for="password" class="block">New Password</label><br />
  <%= password_field_tag 'password', {}, :size => 45 %><br />
  <small>Between 4 and 40 characters</small></p>

  <p><label for="password_confirmation"  class="block">Confirm new password</label><br />
  <%= password_field_tag 'password_confirmation', {}, :size => 45 %></p>

  <%= submit_tag 'Change password' %>

<% end %>

app/views/roles/_role.html.erbの編集。

  • 「ビュー」をクリック
  • 「roles」にカーソルを合わせて右クリック
  • 「新規作成」→「ERBファイル」
  • ファイル名を「_role.html.erb」とし、以下のように編集する。
<li>
  <%= role.rolename %>
    <% if @user.has_role?(role.rolename) %>
      <%= link_to 'remove role', user_role_url(:id => role.id, :user_id => @user.id), :method => :delete %>
    <% else %>
      <%= link_to 'assign role', user_role_url(:id => role.id, :user_id => @user.id), :method => :put %>
    <% end %>
</li>

app/views/roles/index.html.erb:

<h2>Roles for <%=h @user.login.capitalize %></h2>

<h3>Roles assigned:</h3>
<ul><%= render :partial => 'role', :collection => @user.roles %></ul>

<h3>Roles available:</h3>
<ul><%= render :partial => 'role', :collection => (@all_roles - @user.roles) %></ul>

他のapp/views/rolesのビューに関しては削除する。

app/views/sessions/new.html.erbの編集。

<h2>Login with User ID and Password:</h2>
<% form_tag session_path do %>
<p><label for="login">Login</label><br/>
<%= text_field_tag 'login' %></p>

<p><label for="password">Password</label><br />
<%= password_field_tag 'password' %></p>

<p><label for="remember_me">Remember me:</label>
<%= check_box_tag 'remember_me' %></p>

<p><%= submit_tag 'Log in' %><%= link_to "Sign Up", new_user_path %></p>
<% end %>

app/views/user_mailer/activation.html.erbの編集。これがメールの本文。

<%=h @user.login %>, your account has been activated.  To visit the site, follow the link below:

  <%= @url %>

app/views/user_mailer/forgot_password.html.erbの編集。これがメールの本文。

<%=h @user.login %>, to reset your password, please visit

  <%= @url %>

app/views/user_mailer/reset_password.html.erbの編集。これがメールの本文。

<%=h @user.login %>, Your password has been reset

app/views/user_mailer/signup_notification.html.erbの編集。これがメールの本文。

Your account has been created.

  Username: <%=h @user.login %>

Visit this url to activate your account:

  <%= @url %>

app/views/users/_user.html.erbの編集。

<tr class="<%= cycle('odd', 'even') %>">
  <td><%=h user.login %></td>
  <td><%=h user.email %></td>
  <td><%= user.enabled ? 'yes' : 'no' %>
      <% if user.enabled %>
        <%= link_to('disable', user_path(user.id), :method => :delete) %>
      <% else %>
        <%= link_to('enable', enable_user_path(user.id), :method => :put) %>
      <% end %>
  </td>
  <td><%=h @numOfUserRooms[user.id].nil? ? 0 : @numOfUserRooms[user.id] -%> rooms</td>
</tr>

app/views/users/edit.html.erbの編集。

<h2>Edit Your Account</h2>
<p><%= link_to 'Show Profile', user_path(@user) %> | <%= link_to 'Change Password', change_password_path %></p>
<%= error_messages_for :user %>

<% form_for :user, :url => user_url(@user), :html => { :method => :put } do |f| %>
  <p>Email:<br /><%= f.text_field :email, :size => 60 %></p>
 
<%= submit_tag 'Save' %>
<% end %>

app/views/users/index.html.erbの編集。

<h2>All Users</h2>
<table>
  <tr>
    <th>Username</th>
    <th>Email</th>
    <th>Enabled?</th>
    <th>Rooms</th>
  </tr>
    <%= render :partial => 'user', :collection => @users -%>
</table>

app/views/users/new.html.erbの編集。

<%= error_messages_for :user %>
<% form_for :user, :url => users_path do |f| %>
<p><label for="login">Login</label><br/>
<%= f.text_field :login %></p>

<p><label for="email">Email</label><br/>
<%= f.text_field :email %></p>

<p><label for="password">Password</label><br/>
<%= f.password_field :password %></p>

<p><label for="password_confirmation">Confirm Password</label><br/>
<%= f.password_field :password_confirmation %></p>

<p><%= submit_tag 'Sign up' %></p>
<% end %>

app/views/users/show.html.erbの編集。

<h2>User: <%=h @user.login %></h2>
<% if @user.is_server_admin? -%>
<h3>管理者操作</h3>
<ul>
  <li><%= link_to "利用者一覧", users_path %></li>
</ul>
<% end -%>
<h3>登録情報</h3>
<ul>
  <li><%= link_to "登録情報の編集", edit_user_path(current_user) %></li>
  <li><%= link_to "パスワードの変更", change_password_path %></li>
</ul>
<h3>調査室</h3>
<p><%= link_to "調査室を開設する", new_room_path -%></p>
<ul>
  <% @myRooms.each do |room | -%>
  <li><%= link_to "#{room.roomname}", room_path(room.id) -%> </li>
  <%  end %>
</ul>
<p>Joined on: <%= @user.created_at.to_s(:long) %></p>

app/views/rooms/new.html.erbの編集。

<h1>New room</h1>

<% form_for(@room) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :roomname %><br />
    <%= f.text_field :roomname %>
  </p>
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

<%= link_to 'Back', user_path(current_user.id) %>

app/views/rooms/edit.html.erbの編集。

<h1>Editing room</h1>

<% form_for(@room) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :roomname %><br />
    <%= f.text_field :roomname %>
  </p>
  <p>
    <%= f.submit 'Update' %>
  </p>
<% end %>

<%= link_to 'Show', @room %> |
<%= link_to 'Back', user_path(current_user.id) %>

app/views/rooms/show.html.erbの編集。

<h2><%=h @room.roomname -%></h2>


<ul>
  <li><%= link_to '調査室の編集', edit_room_path(@room) -%></li>
  <li><%= link_to '調査室の削除', room_path(@room), :method => 'DELETE' -%></li>
  <li><%= link_to 'トップページに戻る', user_path(current_user.id) -%></li>
</ul>

routes.rbの編集。_

最後にconfig/routes.rbを編集する。「ActionController::Routing::Routes.draw do |map|」から「end」の間に以下を追加する。

  # root
  map.root  :controller => "users", :action => "show"

  # login/signup
  map.signup '/signup', :controller => 'users', :action => 'new'
  map.login '/login', :controller => 'sessions', :action => 'new'
  map.logout '/logout', :controller => 'sessions', :action => 'destroy'
  map.activate '/activate/:id', :controller => 'accounts', :action => 'show'
  map.forgot_password '/forgot_password', :controller => 'passwords', :action => 'new'
  map.reset_password '/reset_password/:id', :controller => 'passwords', :action => 'edit'
  map.change_password '/change_password', :controller => 'accounts', :action => 'edit'

  map.resources :users, :member => { :enable => :put } do |users|
      users.resource :account
      users.resources :roles
  end
 
  map.resource :session
  map.resource :password
  map.resources :rooms

public/index.htmlを削除しないとmap.rootで設定したページがトップページにならないので注意。初期ユーザー ID:ServerAdimin、Password:adminでログインできる。Signupした場合は、開発環境ならばWebrickのログにメールが表示される。

サイトを立ち上げるために、データベースを作る必要があるので、マイグレーションを実行する。

  • プロジェクト名にカーソルを合わせ右クリック「Rakeタスクを実行」→「db:reset」を選択し、実行。
  • fixturesを使っている場合は
    • test/fixtures/以下のroles.yml、rooms.yml, users.yml, permissions.ymlの中身を確認しておくこと、不要なら空っぽにしておく
    • プロジェクト名にカーソルを合わせ右クリック「Rakeタスクを実行」
    • 「db:fixtures:load」を選択し、実行。

あとは、ユーザ認証が必要なコントローラーにbefore_filterを付け加えるだけ。

  • 「before_filter :login_required」ならば、ログインしないとそのページは見れない
  • 「before_filter :check_server_administrator_role」ならば、ログインかつ湯ユーザー ServerAdmin じゃないとそのページは見れない
  • 「before_filter :not_logged_in_required」ならば、ログインしていない状態でないとそのページが見れない。

各種フィルターについては、lib/authenticated_system.rbを参照。新しいフィルターを付け加える場合も、上記ファイルに付け加える。

既存のプロジェクトの修正_

次の点に注意する。

  • もともとUserモデルが無かった場合は、既存のモデルとUserモデル間の関係をちゃんと定義すること
  • ログインしているユーザの情報はcurrent_userというメソッド(lib/authenticated_system.rbで定義されている)で取得できる。

公開するときのメモ_

  • 環境変数 RAILS_ENVを「production」に設定しておくこと。
    • Cシェル系の場合
      setenv RAILS_ENV production
      
    • bash系の場合
      RAILS_ENV=production
      export RAILS_ENV
      
  • passerngerを用いた公開の場合、ソース更新の度にApacheを再起動する必要がある
  • メールに関するエラーはpostfixのログ、CGIエラーはapacheのログ、Railsのエラーは、log/production.log。
  • postfixのログで「fatal: Recipient addresses must be specified on the command line or via the -t option」とでていたら、sendmailの設定ミス。mail.rbの設定を見直すこと。
    # Email settings
    ActionMailer::Base.delivery_method = :sendmail 
    ActionMailer::Base.sendmail_settings = {
      :location => "/usr/sbin/sendmail -t"
    
  • postfixのログで「lost connection after HELO from localhost[127.0.0.1]」というエラーがでたら、SMTPではなく、sendmailを使った方が良い。

戻る_