restful_authenticationでパスワードを忘れた時の処理を考える

パスワードは必ず忘れられる運命にある...。開発者側の立場であれば「パスワードくらい自分でちゃんと管理しておくべき」と言いたくなる。しかし、自分がユーザーであれば、パスワードを忘れて不便を感じている時「パスワードを忘れた場合」なんてリンクがあると、多少は救われた気がする。そこをクリックすれば、パスワードをリセットなり、再発行して、再びログインできることを夢見ている...。
ところで、restful_authenticationには「パスワードを忘れた場合」の処理は特にない。特に無いけども、最初にユーザー登録した時、アクティベーションを促すメールで、ユーザー名とパスワードも送信している。開発者側の立場であれば「過去にメールを送信しています。ご自分で検索してみてください。」で終わりにしたい。しかし、自分がユーザーであれば、「過去のメールは消えてしまった。」とか「自分のパソコンじゃないから検索できない」とかいろいろ理由があって、「ご自分で検索してみてください。」を見た日にはガックリくる...。
やはり、パスワードをリセット、または再発行する仕組みは必要か...。

アクティベーションを利用する

その仕組みを、restful_authenticationでいかに簡単に実現するか悩んでみて、アクティベーションを利用する方法を思い付いた。まずは既存の動作を再確認してみた。

restful_authenticationのアクティベーションの流れ
  • 新規ユーザー登録直後に、登録されたメールアドレス宛に確認メールを自動送信する。
  • そのメールに記載のURLリンクをクリックすることで、新規ユーザーを利用可能な状態にする。
  • URLリンクをクリックしたアクティベーション直後のwebページでは、そのユーザーはログイン状態になっている。
usersテーブルの変化
ユーザーの状態 activation_code activated_at
新規ユーザー登録直後 07db98435f337e7acbe5b485dc5eebed4a30c2ec
アクティベーション完了 2008-07-28 02:57:05


以上のことから、以下の手順でやってみることに。

パスワード再設定の手順
  • 「パスワードを忘れた場合」のリンクをクリックしたら...
    • 登録したメールアドレスを入力してもらう。
    • 新規ユーザー登録直後と同等の状態に戻して、再度アクティベーションのメールを送信する。
    • そのユーザーはアクティベーションすることでログイン状態になるので、その状態でパスワードを変更してもらう。

アクティベーション前の状態にするコード

  • まずは「Forget password >>」のリンクを作った。
<%# ---------- app/views/sessions/new.html.erb ---------- %>
...(中略)...
<p><%= link_to 'Signup >>', new_user_path %></p>
<p><%= link_to 'Forget password >>', input_email_address_new_user_path %></p>
  • input_email_address、deactivateアクション用のルートを設定した。
# ---------- config/routes.rb ----------
ActionController::Routing::Routes.draw do |map|
  map.resources :users, :new=>{:input_email_address=>:get, :deactivate=>:put}

  map.resource :session

  map.resources :todos

  map.activate '/activate/:activation_code', :controller => 'users', :action => 'activate'
...(中略)...
  • input_email_addressアクションで、メールアドレスを入力してもらう。
  • deactivateアクションで、入力したメールアドレスが存在すれば、アクティベーション前の状態に戻す。
# ---------- app/controllers/users_controller.rb ----------
...(中略)...
  def input_email_address
  end

  def deactivate
    if forget_user = User.find_by_email(params[:user][:email])
      forget_user.deactivate
    end
    flash[:notice] = "Email is sent to your address!"
    render :action => 'input_email_address'
  end
end
<%# ---------- app/views/users/input_email_address.html.erb ---------- %>
<% form_for :user, :url=>deactivate_new_user_path, :html=>{:method=>:put} do |f| %>
  <%= f.error_messages %>

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

  <%= f.submit 'Send' %>

<% end %>


<p><%= link_to '<< Cancel', new_session_path %></p>
# ---------- app/models/user.rb ----------
require 'digest/sha1'
class User < ActiveRecord::Base
...(中略)...
  def deactivate
    @activated = false
    self.activated_at = nil
    self.make_activation_code
    
    # 引数をfalseにすると、validationが無効になる
    # save(perform_validation=true)
    save(false)
  end
...(中略)...
  • saveメソッド後のUserモデルを監視して、必要なメールを送信する。
# ---------- app/models/user_observer.rb ----------
class UserObserver < ActiveRecord::Observer
  def after_create(user)
    UserMailer.deliver_signup_notification(user)
  end

  def after_save(user)
    return if user.recently_activated?.nil?
    UserMailer.deliver_activation(user) if user.recently_activated?
    UserMailer.deliver_change_password_notification(user) unless user.recently_activated?
  end
end
  • パスワード変更を促すメール送信用のメソッドを追加した。
# ---------- app/models/user_mailer.rb ----------
class UserMailer < ActionMailer::Base
...(中略)...
  def change_password_notification(user)
    setup_email(user)
    @subject    += 'Please change your password'
    @body[:url]  = "http://localhost:3000/activate/#{user.activation_code}"
  end
...(中略)...
  • パスワード変更を促すメールの雛形を追加した。
<%# ---------- app/views/user_mailer/change_password_notification.html.erb ---------- %>
Visit this url to change your password:

  <%= @url %>


以上で、ひとまず完了した感じ。試してみる。

パスワードを忘れた場合の動作確認

  • Forget passwordのリンクをクリックして...

  • メールアドレスを入力して送信ボタンを押すと...

  • メールを送信したというメッセージが表示された。順調。


  • そして、パスワード変更を促すメールが届いた!
	差出人: 	my_account@gmail.com
	件名: 	[YOURSITE] Please change your password
	日時: 	2008年7月28日 17:43:47:JST
	宛先: 	test_account@mail.com

Visit this url to change your password:

 http://localhost:3000/activate/c76f15176d68b075cadca3be506ccae5d98f943b
  • メール記載のURLリンクをクリックすると、welcomページへログイン状態でアクセスできた。
  • この状態でユーザーはパスワードを自由に変更することが出来るので、再設定すればパスワード忘れの問題は解決する。

ユーザー情報を編集するコード

肝心なユーザー情報を変更する部分の実装がまだだった...。

  • Logoutリンクの左に、ユーザー名のリンクを表示して、クリックするとユーザー情報の編集ページにアクセスするようにしてみた。
<%# ---------- app/views/layouts/application.html.erb ---------- %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title><%= controller.controller_name %>: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
</head>
<body>

<div align="right">
  <% if logged_in? %>
    <%= link_to current_user.login, edit_user_path(current_user) %> |
  <% end %>
  
  <%= link_to_if logged_in?, 'Logout >>', session_path, :method => :delete do
      end %>
</div>

<p style="color: green;background: lightgreen;"><%= flash[:notice] %></p>
<p style="color: red;background: pink;"><%= flash[:alert] %></p>

<%= yield  %>

</body>
</html>
# ---------- app/controllers/users_controller.rb ----------
class UsersController < ApplicationController
...(中略)...
  def edit
  end

  def update
    updated_at = current_user.updated_at
    params[:user].delete(:email)
    if current_user.update_attributes(params[:user])
      if current_user.updated_at != updated_at
        flash[:notice] = 'User was successfully updated.'
        redirect_to(todos_path)
      else
        flash.now[:alert] = 'Nothing was changed.'
        render :action => 'edit'
      end
    else
      render :action => 'edit'
    end
  end
...(中略)...
  • メールアドレスだけは変更できないようにした。
<%# ---------- app/views/users/edit.html.erb ---------- %>
<% form_for current_user do |f| %>
<%= f.error_messages %>

<p><label for="login">Login</label><br/>
<%= f.text_field :login, :autocomplete=>'off' %></p>

<p><label for="email">Email</label><br/>
<%= f.text_field :email, :autocomplete=>'off', :disabled=>true %></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><%= f.submit %></p>
<% end %>


<p><%= link_to '<< Cancel', todos_path %></p>

これでメール認証から、パスワードを自由に変更できるようになった!