NetBeansで作成中のRailsプロジェクトにRestful Authenticationを追加する(その2)
- はじめに
- 今回の前提
- 認証モデル
- Restful Authenticationのインストール
- UserモデルとSessionモデルの生成
- room、rolesとpermissionsの設定
- コントローラーの編集
- ビューの編集
- routes.rbの編集。
- 既存のプロジェクトの修正
- 公開するときのメモ
- 戻る
はじめに_
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
- Cシェル系の場合
- 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を使った方が良い。