読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Rails4でDeviseを使ってゲスト(Guest)ユーザーを作成する

Rails中級 Authentication

ユーザーエクスペリエンスを向上させるために、ユーザー情報を登録しなくてもゲストユーザー(Guest)機能を実装し、個人情報を入力しなくてもゲストユーザーとしてアプリを使えるようにしてみます。
認証機能にはDeviseというgemを使って実装します。


動作確認

  • Rails 4.1
  • Devise 3.3.0

目次

  1. Deviseでユーザー認証
  2. 簡易なToDoアプリを作成
  3. ゲストユーザー機能の作成

1. Deviseでユーザー認証

他の記事でDeviseのインストールについて細かく記載しているので、ここではパパッとDeviseをインストールし、認証機能を追加します。
Railsプロジェクトを作成

rails new guest_with_devise_test
cd guest_with_devise_test

welcomeページとコントローラーを作成

rails g controller welcome index

deviseをインストール

echo "gem 'devise'" >> Gemfile
bundle install
rails g devise:install

deviseの設定

# config/environments/development.rb
Rails.application.configure do
  # devise
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
end

# config/routes.rb
Rails.application.routes.draw do
  root 'welcome#index'
end

# app/views/layouts/application.html.erb
...
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

<%= yield %>

</body>
</html>

deviseでUserモデルの作成

rails g devise User username:string
rake db:migrate

レイアウトファイルにヘッダーの追加

# app/views/layouts/application.html.erb
...
<body>
<header>
  <nav>
    <% if user_signed_in? %>
      Logged in as <strong><%= current_user.email %></strong>.
      <%= link_to "プロフィール変更", edit_user_registration_path %> |
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <% else %>
      <%= link_to "ユーザー登録", new_user_registration_path %> |
      <%= link_to "ログイン", new_user_session_path %>
    <% end %>
  </nav>
</header>

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

<%= yield %>

</body>
</html>

ユーザ名(username)を表示するようにする。

rails g devise:views

# 下記の2つのファイルにusername入力欄を追加する
# - app/views/devise/registrations/edit.html.erb
# - app/views/devise/registrations/new.html.erb
# username入力欄
<div><%= f.label :username %><br />
<%= f.text_field :username %></div>


# DeviseのStrongParametersにusernameを許可するように追加する
class ApplicationController < ActionController::Base
  ...

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_in) << :username
      devise_parameter_sanitizer.for(:sign_up) << :username
      devise_parameter_sanitizer.for(:account_update) << :username
    end
end

サーバーを起動して画面を確認する

rails s

ユーザー登録を行いログインができました。
f:id:nipe880324:20141125010723p:plain:w380




2. 簡易なToDoアプリを作成

ToDo Listアプリケーションの雛形をScaffoldで作成する

# Scaffoldを実行する
rails g scaffold task user_id:integer name:string complete:boolean


# マイグレーションファイルを修正する
# db/migrate/20141124105647_create_tasks.rb
class CreateTasks < ActiveRecord::Migration
  def change
    create_table :tasks do |t|
      t.integer :user_id
      t.string :name
      # 修正箇所
      t.boolean :complete, default: false, null: false

      t.timestamps
    end
  end
end


# マイグレートを実施
rake db:migrate

モデルのアソシエーションを追加する

# app/models/user.rb
has_many :tasks, dependent: :destroy

# app/models/task.rb
belongs_to :user

コントローラーを修正する。全体的にガリガリと書き直す。

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    if current_user
      @incomplete_tasks = current_user.tasks.where(complete: false)
      @complete_tasks = current_user.tasks.where(complete: true)
    end
  end

  def create
    @task = current_user.tasks.create!(task_params)
    redirect_to tasks_url
  end

  def update
    @task = current_user.tasks.find(params[:id])
    @task.update!(task_params)
    respond_to do |format|
      format.html { redirect_to tasks_url }
      format.js
    end
  end

  def destroy
    @task = current_user.tasks.find(params[:id])
    @task.destroy
    respond_to do |format|
      format.html { redirect_to tasks_url }
      format.js
    end
  end

  private

    def task_params
      params.require(:task).permit(:name, :complete)
    end
end

Welcomeページにアクセスした場合に、ログインしている場合は、タスク一覧画面にリダイレクトさせるようにする。

# app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    redirect_to tasks_url if current_user
  end
end

Welcomeページを作成する

# app/views/welcome/index.html.erb
<h1>ようこそ ToDo List アプリケーションへ!</h1>

<p><%= button_to "無料体験", new_user_registration_path, method: :get %></p>

<p>既にユーザーアカウントを持っている方は <%= link_to "ログイン", new_user_session_path %></p>

一覧ページを修正する

# app/views/tasks/index.html.erb
<h1>ToDo List</h1>

<%= form_for Task.new do |f| %>
  <%= f.text_field :name %>
  <%= f.submit "タスクを追加" %>
<% end %>

<% if @incomplete_tasks.empty? && @complete_tasks.empty? %>
  <p>Currently no tasks. Add one above.</p>
<% else %>
  <h2>未完了のタスク</h2>
  <div class="tasks" id="incomplete_tasks">
    <%= render @incomplete_tasks %>
  </div>

  <h2>完了したタスク</h2>
  <div class="tasks" id="complete_tasks">
    <%= render @complete_tasks %>
  </div>
<% end %>

タスクの部分テンプレートを作成する。

# app/views/tasks/_task.html.erb
<%= form_for task, remote: true do |f| %>
  <%= f.check_box :complete %>
  <%= f.label :complete, task.name %>
  <%= link_to "(削除)", task, method: :delete, data: {confirm: "Are you sure?"}, remote: true %>
<% end %>

Ajaxの更新処理を実装する。

# app/assets/javascripts/tasks.js.coffee
$ ->
  $('.edit_task input[type=checkbox]').click ->
    $(this).parent('form').submit()


# app/views/tasks/update.js.erb
<% if @task.complete? %>
  $('#edit_task_<%= @task.id %>').appendTo('#complete_tasks');
<% else %>
  $('#edit_task_<%= @task.id %>').appendTo('#incomplete_tasks');
<% end %>

Ajaxで削除処理を実装する。

# app/views/tasks/destroy.js.erb
$('#edit_task_<%= @task.id %>').remove();

タスク画面のスタイルを修正する。

# app/assets/stylesheets/tasks.css.scss
h2 {
  font-size: 16px;
  margin-top: 20px;
}

#incomplete_tasks form {
  margin: 5px 0;
}

#complete_tasks form {
  font-size: 12px;
  color: #777;
  margin: 3px 0;
  label { text-decoration: line-through; }
}

.edit_task {
  a {
    color: #999;
    font-size: 12px;
    text-decoration: none;
    &:hover { text-decoration: underline; }
  }
}

.loading {
  margin-left: 10px;
  color: #777;
}

では、rails sでサーバーを再起動し、画面を確認してみます。
タスク一覧画面でタスクの登録や更新、削除ができます。
f:id:nipe880324:20141125011515p:plain:w420


しかし、現状はWelcomeページの「無料体験」ボタンを押すと、ユーザー登録画面(Sing up)に遷移してしまいます。
f:id:nipe880324:20141125011710p:plain:w420

これを、ゲストユーザーを作成するようにし、アプリケーションをを使えるようにしましょう。




3. ゲストユーザー機能の作成

まず、Guestユーザーフラグを追加します。

# マイグレーションファイルの作成
rails g migration add_guest_to_users guest:boolean

# マイグレーションファイルの修正
# db/migrate/20141124113946_add_guest_to_users.rb
class AddGuestToUsers < ActiveRecord::Migration
  def change
    # defaultはfalse(通常のユーザー)
    add_column :users, :guest, :boolean, default: false
  end
end


# マイグレーションを実施
rake db:migrate

「無料体験」ボタンを押したらゲストユーザーをアクションを呼ぶように変更します。

# app/views/welcome/index.html.erb
<p><%= button_to "無料体験", welcome_guest_path %></p>

ルートを追加します。

# config/routes.rb
post 'welcome/guest' => 'welcome#guest'

そして、コントローラーにアクションを実装します。

# app/controllers/welcome_controller.rb
def guest
  guest_user # guest_userを作成する
  redirect_to tasks_url
end

さらに、ApplicationControllerにゲストユーザーの作成ロジックを記載します。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  helper_method :guest_user?, :current_or_guest_user

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_in) << :username
      devise_parameter_sanitizer.for(:sign_up) << :username
      devise_parameter_sanitizer.for(:account_update) << :username
    end

    # ログインしている場合は、 current_userを返す / していない場合は guest_user を返す
    # current_userを置き換えることにより、Guestと通常のユーザーを透過的に扱えるようになる
    def current_or_guest_user
      if current_user
        if session[:guest_user_id] && session[:guest_user_id] != current_user.id
          logging_in
          guest_user(with_retry = false).try(:destroy)
          session[:guest_user_id] = nil
        end
        current_user
      else
        guest_user
      end
    end

    # 現在のセッションと関連づく guest_user オブジェクトを探す
    def guest_user(with_retry = true)
      @cached_guest_user ||= User.find(session[:guest_user_id] ||= create_guest_user.id)
    rescue ActiveRecord::RecordNotFound # if session[:guest_user_id] invalid
       session[:guest_user_id] = nil
       guest_user if with_retry
    end

    def guest_user?
      current_user && current_user.guest?
    end

    # ログインしていない、もしくは、Guestユーザーの場合、ルートにリダイレクトする
    def authenticate_no_user_or_guest!
      redirect_to root_url if current_user.nil? || guest_user?
    end

    # ユーザーがログインした際に一度だけ呼ばれる
    # 通常のユーザーが作成され、Guestユーザーは削除されるので、
    # Guest時に作成したデータを通常のユーザーに渡すなどの処理をする
    def logging_in
      # taskのuser_idをGuestから通常のユーザーに移す
      guest_user.move_to(current_user) 
    end

    # Guestユーザーを作成する
    def create_guest_user
      guest = User.new_guest
      guest.save!(:validate => false)
      session[:guest_user_id] = guest.id
      guest
    end

end

そして、UserモデルにApplicationControllerから呼ばれるメソッドを追加します。

# app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  has_many :tasks, dependent: :destroy

  validates :username, presence: true

  def self.new_guest
    new do |u|
      u.username = "Guest"
      u.email    = "guest_#{Time.now.to_i}#{rand(100)}@example.com"
      u.guest    = true
    end
  end

  def move_to(user)
    tasks.update_all(user_id: user.id)
  end
end

Guestユーザーのときのヘッダーの表示内容を追記します。
curret_usercurrent_or_guest_userに変更します。

....
<header>
  <nav>
    <% if user_signed_in? %>
      Logged in as <strong><%= current_or_guest_user.username %></strong>.
      <% if guest_user? %>
        <%= link_to "メンバーになる", new_user_registration_path %>
      <% else %>
        <%= link_to "プロフィール変更", edit_user_registration_path %> |
        <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
      <% end %>
    <% else %>
      <%= link_to "ユーザー登録", new_user_registration_path %> |
      <%= link_to "ログイン", new_user_session_path %>
    <% end %>
  </nav>
</header>

...

コントローラー内のcurret_usercurrent_or_guest_userに変更します。

# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    if current_or_guest_user
      @incomplete_tasks = current_or_guest_user.tasks.where(complete: false)
      @complete_tasks = current_or_guest_user.tasks.where(complete: true)
    end
  end

  def create
    @task = current_or_guest_user.tasks.create!(task_params)
    redirect_to tasks_url
  end

  def update
    @task = current_or_guest_user.tasks.find(params[:id])
    @task.update!(task_params)
    respond_to do |format|
      format.html { redirect_to tasks_url }
      format.js
    end
  end

  def destroy
    @task = current_or_guest_user.tasks.find(params[:id])
    @task.destroy
    respond_to do |format|
      format.html { redirect_to tasks_url }
      format.js
    end
  end

  ...
end

そして、Guestユーザーでログイン時にユーザー登録画面に遷移しようとするとDevise::RegistrationsControllerによりリダイレクトされてしまうので、オーバーライドします。

# app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  # no_user      -> ok: new, create          / NG: edit, update, cancel
  # guest        -> ok: new, create          / NG: edit, update, cancel
  # current_user -> ok: edit, update, cancel / NG: new, create
  before_action :authenticate_no_user_or_guest!, except: [:new, :create]
  skip_before_filter :require_no_authentication, only:   [:new, :create], if:     :guest_user?
  before_action      :require_no_authentication, only:   [:new, :create], unless: :guest_user?
end

そして、ルーティングでこの作成したコントローラーを呼ぶように変更します。

# config/routes.rb
devise_for :users, :controllers => { registrations: "registrations" }

最後に、DeviseはWardenというRack Middlewareを使っているのですが、authenticate_user!をGuestユーザーでも使えるようにするために、initializerに下記2つを追加します。

# config/initializers/warden_strategies.rb
Warden::Strategies.add(:guest_user) do
  def valid?
    session[:guest_user_id].present?
  end

  def authenticate!
    guest = User.find_by(id: session[:guest_user_id])
    success!(guest) if guest.present?
  end
end
# config/initializers/devise.rb
Devise.setup do |config|
  ...
  # ==> Warden configuration
  config.warden do |manager|
    manager.default_strategies(scope: :user).unshift :guest_user
  end
  ...
end

では、rails sでサーバーを再起動し、ゲストユーザーが使えるか確認してみましょう。
「無料体験」ボタンを押すとヘッダー部に"Guest"という名前でログインしていることがわかると思います。また、タスクの追加、更新、削除ができます。
f:id:nipe880324:20141125013539p:plain:w420

では、「メンバーになる」リンクからユーザー登録をします。
f:id:nipe880324:20141125013547p:plain:w420

すると、ヘッダー上部にユーザー名が表示され、通常のユーザーとしてログインができました。
f:id:nipe880324:20141125013552p:plain:w420


最後に、ユーザー登録をすればゲストユーザーは削除されるのですが、ユーザー登録をしないというユースケースも充分に考えられます。その場合、DBに使われないゲストユーザーの情報が残ってしまうため、下記のタスクで作成してから1週間以上経過しているゲストユーザーを削除するようにします。
クーロンなどで設定し、定期的に実行すると良いです。

# lib/tasks/guests.rake
namespace :guests do
  desc "Remove guest accounts more than a week old."
  task :cleanup => :environment do
    User.where(guest: :true).where("created_at < ?", 1.week.ago).destroy_all
  end
end


以上です。