何度も挑戦して挫折している Restful Authentication with all the bells and whistles (new 9/05/08) をやってみる。ただし、OpenIDの導入前まで。
チュートリアルの実施環境
元のチュートリアルはMySQLを使っているけど、私はSQLite3を使った。
% uname -a Linux pi 2.6.30-2-686 #1 SMP Fri Dec 4 00:53:20 UTC 2009 i686 GNU/Linux % more /proc/version Linux version 2.6.30-2-686 (Debian 2.6.30-8squeeze1) (dannf@debian.org) (gcc ver sion 4.3.4 (Debian 4.3.4-6) ) #1 SMP Fri Dec 4 00:53:20 UTC 2009 % ruby -v ruby 1.8.7 (2009-06-12 patchlevel 174) [i486-linux] % gem1.8 --version 1.3.5 % rails -v Rails 2.3.5 % sqlite3 --version 3.6.21
restful authenticationのインストール
まず、新しいプロジェクトを作成し、restful authenticationをインストールする。
% rails myproject % cd myproject % ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/
Pageリソースを生成する。その後、restful_authenticationプラグインで使うファイルを生成する。
% ruby script/generate scaffold Page title:string body:text % ruby script/generate authenticated user sessions --include-activation
上記コマンドでセッション用のコントローラーとビューができているはず。その後、application_controller.rbに"include AuthenticatedSystem"が付け加えられているかをチェックする。チェックすると付け加えられていないので付け加える。
メール送信設定
次にconfig/initalizersにmail.rbを作成する。以下はチュートリアルの例。
mail.rb:
# Email settings ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { :address => "mail.yourapplication.com", :port => 25, :domain => "yourapplication.com", :authentication => :login, :user_name => "mail@yourapplication.com", :password => "yourapplicationpassword" }
mail.rb(localhostのメールサーバーをポート番号25で使う場合)
# Email settings ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { :address => "localhost", :port => 25 }
sendmailを使ってもメールを送ることができる。その場合は、以下のようになる。
mail.rb:
# 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
次に、app/models以下にある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] = "http://localhost:3000/activate/#{user.activation_code}" end def activation(user) setup_email(user) @subject += 'Your account has been activated!' @body[:url] = "http://localhost:3000/" end def forgot_password(user) setup_email(user) @subject += 'You have requested to change your password' @body[:url] = "http://localhost:3000/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 = "mail@yourapplication.com" @subject = "YourApplication - " @sent_on = Time.now @body[:user] = user end end
チュートリアルでは上のようになっているが、モデル内に直接URLを書くのは便利ではないので、定数として定義しそれを使うようにした方が良い。
そうする場合は、config/environment.rbに定数を定義するか、config/environments/development.rb(開発用設定ファイル)かconfig/environments/development.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
そして、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
RolesとPermissionsの設定
rolesとpermissionsを設定する。
% ruby script/generate scaffold Role rolename:string % ruby script/generate model Permission
db/migrate以下にあるXXX_create_permissions.rbを編集する。
class CreatePermissions < ActiveRecord::Migration def self.up create_table :permissions do |t| t.integer :role_id, :user_id, :null => false t.timestamps end end def self.down drop_table :permissions end end
元のチュートリアルでは、XXX_create_permissions.rbファイルの中で初期データをセットしていたが、Rails 2.3.4からそれをやめるようになったらしい(情報元:ひげろぐ:Rails 2.3.4で追加されたseeds.rbについて)
db/seeds.rbを以下のように書く。
#Make sure the role migration file was generated first Role.create(:rolename => 'administrator') #Then, add default admin user #Be sure change the password later or in this migration file user = User.new user.login = "admin" user.email = "info@yourapplication.com" user.password = "admin" user.password_confirmation = "admin" user.save(false) user.send(:activate!) role = Role.find_by_rolename('administrator') user = User.find_by_login('admin') permission = Permission.new permission.role = role permission.user = user permission.save(false)
controllerを編集し始める前に、db/migrate以下にあるXXX_create_users.rbにenabledとpassword_reset_code を付け加える。
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
次に、app/models以下のファイルをいくつか変更しなければならない。はじめに、rolesとusersの間の関係を定義する。rolesとusersは多対多の関係なので、has_many :through宣言で関係を示す。
role.rb:
class Role < ActiveRecord::Base has_many :permissions has_many :users, :through => :permissions end
permission.rb
class Permission < ActiveRecord::Base belongs_to :user belongs_to :role end
Userモデルの修正
続いて、以下の変更をuser.rbに行なう。
- メールアドレスの長さチェックに関する変更
- userとrolesの間の関係
- あるユーザーがある役割をもっているかどうかを調べるチェック
- パスワード忘れに関する記述
- ユーザーアクティベートに関するいくつかの変更。
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 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 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
authenticated_system.rbの編集
チュートリアルでは、lib以下にある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 def check_administrator_role check_role('administrator') 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 = RootURL 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
チュートリアルの作者曰く。permission_deniedメソッドは改良の余地があるとのこと。このメソッドの働きは、あるリソースへのアクセス権限がない場合、一つ前に見ていたページに戻るというもの(ただし、自ドメインでない場合は、自ドメインのトップに戻る)。以下、チュートリアルの原文。
The permission_denied method is definitly an area I feel needs improvement. Its just something I threw together so that users would be redirected properly if they tried to access a resource they didn't have permission for. Its designed to redirect back to the last page they were on, unless that page is on another site or has the same address as the resource they're trying to access. Please let me know if you have any suggested improvements for that method.
コントローラーの編集
続いてcontrollerを編集する。チュートリアルでは、restful_authenticationによって生成された多くのactionをそれら自身のコントローラーに移している。目的は、restfulの振る舞いにできる限りそうようにするため。また、将来の拡張しやすいようにしておくため。
まず、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_administrator_role, :only => [:index, :destroy, :enable] def index @users = User.find(:all) end #This show action only allows users to view their own profile def show @user = current_user 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を編集する。
(この方法で設定すると、openid authenticationとの統合が簡単にできる)
# 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を生成する。
% ruby script/generate controller Passwords edit new % ruby script/generate 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_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
ビューの編集
次はビューの編集。まず、最初にこのアプリケーションのレイアウトを作成する。このレイアウトは少なくとも以下を含むこと。
app/view/layout/application.html.erb
<ul> <% if logged_in? %> <li>Logged in as:</li> <li><%= link_to h(current_user.login.capitalize), user_path(current_user) %></li> <ul> <li><%= link_to 'Edit Profile', edit_user_path(current_user) %></li> <li><%= link_to 'Change Password', change_password_path %></li> <li><%= link_to 'Log Out', logout_url %></li> </ul> <% if current_user.has_role?('administrator') %> <li><%= link_to 'Administer Users', users_path %></li> <% end %> <% else %> <li><%= link_to 'Log In', new_session_path %></li> <li><%= link_to 'Sign Up', new_user_path %></li> <li><%= link_to 'Forgot Password?', forgot_password_path %></li> <% end %> </ul>
私は以下のようにファイルを用意した。
<html> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title><%= h @title -%></title> <%= stylesheet_link_tag 'main' %> </head> <body> <ul> <% if logged_in? %> <li>Logged in as:</li> <li><%= link_to h(current_user.login.capitalize), user_path(current_user) %></li> <ul> <li><%= link_to 'Edit Profile', edit_user_path(current_user) %></li> <li><%= link_to 'Change Password', change_password_path %></li> <li><%= link_to 'Log Out', logout_url %></li> </ul> <% if current_user.has_role?('administrator') %> <li><%= link_to 'Administer Users', users_path %></li> <% end %> <% else %> <li><%= link_to 'Log In', new_session_path %></li> <li><%= link_to 'Sign Up', new_user_path %></li> <li><%= link_to 'Forgot Password?', forgot_password_path %></li> <% end %> </ul> <hr> <h1><%= h @title -%></h1> <p><%=h flash[:notice] -%></p> <%= yield %> </body> </html>
app/views/passwords/edit.html.erbの編集。
<% 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>Forgot Password</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の編集。
<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' %> <% unless user == current_user %> <% if user.enabled %> <%= link_to('disable', user_path(user.id), :method => :delete) %> <% else %> <%= link_to('enable', enable_user_path(user.id), :method => :put) %> <% end %> <% end %> </td> <td><%= link_to 'edit roles', user_roles_path(user) %>]</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>Roles</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> <p>Joined on: <%= @user.created_at.to_s(:long) %></p>
routes.rbの編集
最後にconfig/routes.rbを編集する。
ActionController::Routing::Routes.draw do |map| map.root :controller => "pages", :action => "index" 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' # See how all your routes lay out with "rake routes" map.resources :pages map.resources :users, :member => { :enable => :put } do |users| users.resource :account users.resources :roles end map.resource :session map.resource :password # Install the default routes as the lowest priority. map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end
public/index.htmlを削除するのをお忘れなく。
データベースマイグレーション
サイトを立ち上げるために、データベースを作る必要があるので、マイグレーションを実行する。
% rake db:reset % rake db:seed
fixturesを使う方は上記コマンドののちに
% rake db:fixtures:load
認証まわりがうまく追加できて、ログイン後のもろもろを追加したいときに毎回Sign upするのは面倒。そういう場合は、fixturesを使う。
% rake db:fixtures:extract
これで、データベースの中身がtest/fixtures以下に吐き出される。
認証機能の利用法
サーバーを立ち上げると、あなたはユーザ認証システムを使えます。
app/controllers/pages_controller.rbにbofore_filterを付け加えるのを忘れないこと。具体的には、:login_required, :check_administrator_roleや:not_logged_in_required。
新しい、フィルターを付け加えるのも簡単。例えば、:check_moderator_roleというフィルターを付け加えたいときには、authenticated_system.rbに以下を付け加える。もちろん、moderatorという役割をテーブルに付け加えるのを忘れずに。
def check_moderator_role check_role('moderator') end