Devise徹底解説 第2回:DeviseInvitableでユーザー招待機能を実装しよう
はじめに
前回のチュートリアルでは、DeviseをRailsアプリケーションにインストールし、初期設定を行う方法を学びました。今回はその続編として、DeviseInvitableをアプリケーションに統合する手順を詳しく解説していきます。
DeviseInvitableはDeviseと連携して動作する拡張gemです。これを導入すると、ユーザーがメール経由で友人や同僚をアプリケーションに招待できるようになります。特にコラボレーション型のアプリを開発している場合には、非常に有用な機能と言えるでしょう。
DeviseInvitableのセットアップ
まずはGemfileを開き、以下のgemを追加します。
#Gemfile ... gem 'devise_invitable'
次のコマンドで依存関係をインストールします。
bundle install
続いて、ジェネレーターコマンドを実行し、DeviseInvitableの設定オプションをDeviseの設定ファイルに追加しましょう。
rails generate devise_invitable:install
テキストエディタでconfig/initializers/devise.rbを確認すると、新しく追加された設定内容をチェックできます。
次に、UserモデルへDeviseInvitableを追加します。
rails generate devise_invitable User
このコマンドにより、モデルに:invitableフラグが追加され、Userモデルは以下のようになります。
#app/models/user.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
同時に、以下のようなマイグレーションファイルも自動生成されます。
class DeviseInvitableAddToUsers < ActiveRecord::Migration
def up
change_table :users do |t|
t.string :invitation_token
t.datetime :invitation_created_at
t.datetime :invitation_sent_at
t.datetime :invitation_accepted_at
t.integer :invitation_limit
t.references :invited_by, polymorphic: true
t.integer :invitations_count, default: 0
t.index :invitations_count
t.index :invitation_token, unique: true # for invitable
t.index :invited_by_id
end
end
def down
change_table :users do |t|
t.remove_references :invited_by, polymorphic: true
t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
end
end
end
rake db:migrateを実行して、データベースに変更を反映させましょう。これで招待トークンや招待者情報など、招待機能に必要なカラムがusersテーブルに追加されます。
DeviseInvitable用のコントローラー設定
DeviseInvitableで招待メールを送信する際には、いくつかのパラメーターを受け渡す必要があります。そのため、使用するパラメーターを事前に許可(ホワイトリスト化)しておく必要があります。app/controllers/application_controller.rbをテキストエディタで開き、以下のように編集してください。
#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?
protected
def configure_permitted_parameters
added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
devise_parameter_sanitizer.permit :accept_invitation, keys: [:email]
end
end
上記のコードでは、DeviseInvitable向けに:emailが許可されているのが分かりますね。
それでは、コンソールから実際の動作を確認してみましょう。ターミナルでrails consoleを実行し、以下のように入力します。
[1] pry(main)> User.invite!(:email => "johndoe@example.com")
環境によって差異はありますが、以下のような出力が得られるはずです。
[2] pry(main)> User Load (78.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? ORDER BY "users"."id" ASC LIMIT 1 [["email", "johndoe@example.com"]]
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."invitation_token" = ? ORDER BY "users"."id" ASC LIMIT 1 [["invitation_token", "658da470d5fcbb2275f30bc1fb66f5771b889cec2f1e56f536319d2fd1ef4a92"]]
(0.1ms) begin transaction
SQL (67.8ms) INSERT INTO "users" ("email", "encrypted_password", "invitation_token", "invitation_created_at", "invitation_sent_at", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["email", "johndoe@example.com"], ["encrypted_password", "$2a$11$0sLfqvfFDsebcmcQTUXzlOuqNIooL5z8niXeza8OUwNK3gZY/iRum"], ["invitation_token", "658da470d5fcbb2275f30bc1fb66f5771b889cec2f1e56f536319d2fd1ef4a92"], ["invitation_created_at", "2016-10-07 07:41:51.254047"], ["invitation_sent_at", "2016-10-07 07:41:51.254047"], ["created_at", "2016-10-07 07:41:51.255700"], ["updated_at", "2016-10-07 07:41:51.255700"]]
(220.5ms) commit transaction
Rendered /home/kinsomicrote/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/devise_invitable-1.7.0/app/views/devise/mailer/invitation_instructions.html.erb (2.5ms)
Rendered /home/kinsomicrote/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/devise_invitable-1.7.0/app/views/devise/mailer/invitation_instructions.text.erb (88.0ms)
Devise::Mailer#invitation_instructions: processed outbound mail in 247.1ms
Sent mail to johndoe@example.com (74.3ms)
Date: Fri, 07 Oct 2016 08:41:51 +0100
From: please-change-me-at-config-initializers-devise@example.com
Reply-To: please-change-me-at-config-initializers-devise@example.com
To: johndoe@example.com
Message-ID: <57f751bfce8d6_18022ac6c272b12840661@kinsomicrote-X553MA.mail>
Subject: Invitation instructions
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_57f751bfcc725_18022ac6c272b12840524";
charset=UTF-8
Content-Transfer-Encoding: 7bit
----==_mimepart_57f751bfcc725_18022ac6c272b12840524
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit
Hello johndoe@example.com
Someone has invited you to https://localhost:3000/, you can accept it through the link below.
https://localhost:3000/users/invitation/accept?invitation_token=xmW9uRfyafptmeFMmFBy
If you don't want to accept the invitation, please ignore this email.
Your account won't be created until you access the link above and set your password.
----==_mimepart_57f751bfcc725_18022ac6c272b12840524
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: 7bit
<p>Hello johndoe@example.com</p>
<p>Someone has invited you to https://localhost:3000/, you can accept it through the link below.</p>
<p><a href="https://localhost:3000/users/invitation/accept?invitation_token=xmW9uRfyafptmeFMmFBy">Accept invitation</a></p>
<p>If you don't want to accept the invitation, please ignore this email.<br />
Your account won't be created until you access the link above and set your password.</p>
----==_mimepart_57f751bfcc725_18022ac6c272b12840524--
=> #<User:0x00558d875fa798
id: 4,
email: "johndoe@example.com",
encrypted_password: "$2a$11$0sLfqvfFDsebcmcQTUXzlOuqNIooL5z8niXeza8OUwNK3gZY/iRum",
reset_password_token: nil,
reset_password_sent_at: nil,
remember_created_at: nil,
sign_in_count: 0,
current_sign_in_at: nil,
last_sign_in_at: nil,
current_sign_in_ip: nil,
last_sign_in_ip: nil,
created_at: Fri, 07 Oct 2016 07:41:51 UTC +00:00,
updated_at: Fri, 07 Oct 2016 07:41:51 UTC +00:00,
username: nil,
invitation_token: "658da470d5fcbb2275f30bc1fb66f5771b889cec2f1e56f536319d2fd1ef4a92",
invitation_created_at: Fri, 07 Oct 2016 07:41:51 UTC +00:00,
invitation_sent_at: Fri, 07 Oct 2016 07:41:51 UTC +00:00,
invitation_accepted_at: nil,
invitation_limit: nil,
invited_by_id: nil,
invited_by_type: nil,
invitations_count: 0>
[3] pry(main)>
想定どおり、問題なく動作しましたね。招待メールには、受信者が招待を承諾するためのリンクが含まれています。
とはいえ、実際のユーザーにコマンドラインから招待を送らせるわけにはいきません。そこで、DeviseInvitableをフロントエンド側でも使えるように設定することが重要です。手順はとてもシンプルで、ジェネレーターコマンドを実行してビューを生成するだけです。
rails generate devise_invitable:views users
さらに、招待送信用ページ(app/views/users/invitations/new.html.erb)へのリンクを、アプリケーション内のどこかに設置する必要があります。
本アプリケーションでは、ナビゲーションファイルにリンクを追加することにしましょう。筆者の実装例は以下のとおりです。
#app/views/shared/_navigation.html.erb
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Tutsplus Devise', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'Home', root_path %></li>
</ul>
<ul class="nav navbar-nav pull-right">
<% if user_signed_in? %>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<%= current_user.username %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li><%= link_to 'Invite', new_user_invitation_path %></li>
<li><%= link_to 'Profile', edit_user_registration_path %></li>
<li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li>
</ul>
</li>
<% else %>
<li><%= link_to 'Log In', new_user_session_path %></li>
<li><%= link_to 'Sign Up', new_user_registration_path %></li>
<% end %>
</ul>
</div>
</div>
</nav>
DeviseInvitableによって利用可能になったルートを確認したい場合は、rake routes | grep invitコマンドを実行してください。出力は以下のようになります。
cancel_user_registration GET /users/cancel(.:format) devise_invitable/registrations#cancel
user_registration POST /users(.:format) devise_invitable/registrations#create
new_user_registration GET /users/sign_up(.:format) devise_invitable/registrations#new
edit_user_registration GET /users/edit(.:format) devise_invitable/registrations#edit
PATCH /users(.:format) devise_invitable/registrations#update
PUT /users(.:format) devise_invitable/registrations#update
DELETE /users(.:format) devise_invitable/registrations#destroy
accept_user_invitation GET /users/invitation/accept(.:format) devise/invitations#edit
remove_user_invitation GET /users/invitation/remove(.:format) devise/invitations#destroy
user_invitation POST /users/invitation(.:format) devise/invitations#create
new_user_invitation GET /users/invitation/new(.:format) devise/invitations#new
PATCH /users/invitation(.:format) devise/invitations#update
PUT /users/invitation(.:format) devise/invitations#update
それでは、現時点での動作を確認してみましょう。rails serverコマンドでサーバーを起動します。
ブラウザでhttps://localhost:3000/users/invitation/newにアクセスし、表示されたフォームにメールアドレスを入力してボタンをクリックしてください。これで正常に動作するはずです。サーバーのログを見ると、招待を送信した際の出力が記録されており、その中に招待承諾用のリンクが含まれていることを確認できます。
ただ、送信したメールをブラウザ上で直接プレビューできたほうが便利ですよね。次はその方法を見ていきましょう。
Letter_Openerの導入
Letter Openerを使うと、開発環境で送信したメールをデフォルトブラウザでプレビューできます。これにより、開発中にメール配信システムを構築する必要がなくなります。
Gemfileを開き、以下のgemを追加してください。
gem 'letter_opener'
bundle installを実行します。
次に、テキストエディタでconfig/environments/development.rbを開き、以下の行を追加します。
#config/environments/development.rb ... config.action_mailer.delivery_method = :letter_opener end
Railsサーバーを再起動し、再びhttps://localhost:3000/users/invitation/newにアクセスしてフォームを送信してみてください。今度は新しいページがポップアップ表示され、送信された招待メールの内容をその場で確認できるはずです。
サインイン・サインアウトのデフォルトルートを変更する
Deviseを使用した場合、デフォルトではsign_inとsign_outのルートは以下のようになっています。
sign_in:https://localhost:3000/users/sign_insign_out:https://localhost:3000/users/sign_out
これを変更したい場合は、config/routes.rbに以下を追記します。
#config/routes.rb
as :user do
get 'signin' => 'devise/sessions#new'
post 'signin' => 'devise/sessions#create'
delete 'signout' => 'devise/sessions#destroy'
end
これで、https://localhost:3000/signinとしてアクセスできるようになりました。
まとめ
今回は、DeviseInvitableを使ったユーザー招待機能の実装方法を学びました。あわせて、開発環境でメールを手軽にプレビューできるletter_openerというgemについても紹介しました。Deviseにはまだまだ便利な機能がたくさんありますので、興味のある方は公式Wikiもぜひ参照してみてください。最後までお読みいただき、ありがとうございました。
-
Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説
Rubyの内部構造をちょっと覗いてみませんか? それなら、この記事はきっとお役に立ちます。 なぜなら… この記事では、Rubyオブジェクトがメモリ上でどのように配置されているのか、そして内部データ構造を操作してクールなことを実現する方法を、一緒に探検していきます。 シートベルトを締めて、Rubyインタプリタの深淵への旅に出かけましょう! 配列(Array)のメモリレイアウト 配列を作成すると、Rubyはそのデータを保持するためにシステムメモリと、少しのメタデータを確保します。 メタデータには以下が含まれます: 配列のサイズ(要素数) 配列の容量(capacity) クラス情報 オブジェクトの
-
Ruby MRIのソースコードを読んで内部構造を学ぶ方法
Rubyを使い込んでいると、その裏側でどのように動作しているのか気になったことはありませんか? Rubyの内部に深く踏み込む方法の一つが、それを実現しているソースコードを読むことです。C言語がわからなくても、興味深い発見はたくさんあります。 ソースコードはRubyのGitHubリポジトリで公開されています。 理想を言えば、クラス名やメソッド名を簡単に検索できるCodequeryのようなツールを使うのがおすすめです。 コアクラスを探検する 調査のほとんどはルートフォルダで行うことになります。ここには、object.cにあるObjectや、array.cにあるArrayなど、すべてのコアクラスのソ