この記事で実装する機能
- アプリに招待したい人に招待メールを送り、メールの中のURLをクリックした人を新規ユーザーとしてアクティベートする。
- 招待メールの有効期限は24時間
- 招待された人がアカウントをアクティベートした場合、招待したユーザーと招待されたユーザーはお互いをフォローする。
経緯
Railsチュートリアルを一周した後、二週目はチュートリアルのコードを参考にしながら自作アプリを作るのって、割と効率的な学習法だと思います。
受動的に写経するよりも、主体的にコードにアレンジを加えたほうが理解が深まりますしね。
ただ、この学習法には1つ問題があります。
アカウント認証のデファクトっぽいgem「Devise」が使えないことです。
Railsチュートリアルではログイン機能を自作しますが、そのせいでDeviseを導入しようとすると、チュートリアルの中で定義したcurrent_userメソッド
やらhas_secure_password
で自動生成されるauthenticateメソッド
が競合して、色々と問題が起きます。
というか、起きました。
実は、自作アプリで招待機能を実装しようとして、検索上位に出てくるgem devise_invitableを使うためにdeviseを導入しようとしたのですが、
https://qiita.com/himatani/items/907900f1f0d0e4f379b5
結果として、無理でした。
以下のリンクなど色々と試してみたものの、、
https://qiita.com/rrr/items/f4bd8be2772b006987d5
http://ariarichan.hateblo.jp/entry/2016/06/15/235715
結局、「自作のログイン機能を破棄しなければ招待機能を実装できない」という結論に至りました。
「それならいっそ招待機能も自作するわ!!##」ということで車輪の再発明に挑戦してみたところ、チュートリアルの11,12章の知識の組み合わせでなんとかできました。
というわけで、その車輪の実装方法を共有しておこうと思います。
招待機能の作り方
まずは11章、12章でそうしたように、招待機能のmigrationファイルを作成しましょう。
$rails g migration add_invitation_to_user
miragtionファイルの中身はこんな感じ⬇️
invited_byは、招待後に招待したユーザーと招待されたユーザを相互フォローするためのカラムなので、不必要な人はよしなに〜。
class AddInvitationToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :invite_digest, :string
add_column :users, :invited_by, :integer
add_column :users, :invite_sent_at, :datetime
end
end
マイグレート!
$rails db:migrate
からのコントローラ作成。
$rails g controller invitations
中身はこんな感じ⬇︎
class InvitationsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
def new
end
def create
if params[:invitee][:email].blank?
flash[:danger] = "メールアドレスを入力してください。"
render 'new'
elsif User.find_by(email: params[:invitee][:email])
flash.now[:danger] = "そのメールアドレスはすでに招待済みです。"
render 'new'
else
@user = User.create(name: "名無しの招待者", email: params[:invitee][:email].downcase, password: "foobar", invited_by: current_user.id)
@user.create_invite_digest
@user.send_invite_email
flash[:info] = "招待メールを送信しました!"
redirect_to root_url
end
end
def edit
@user.name = nil if @user
end
def update
if params[:user][:password].empty?
@user.errors.add(:password, :blank)
render 'edit'
elsif @user.update_attributes(user_params)
@user.activate
log_in @user
inviter = User.find(@user.invited_by)
@user.follow(inviter)
inviter.follow(@user)
flash[:success] = "ようこそ01Booksへ!"
redirect_to @user
else
render "edit"
end
end
private
def user_params
params.require(:user).permit(:name, :password, :password_confirmation)
end
#beforeフィルタ
def get_user
@user = User.find_by(email: params[:email])
end
#正しいユーザーかどうか確認する
def valid_user
unless (@user && !@user.activated? &&
@user.authenticated?(:invite, params[:id])) #params[:id]はメールアドレスに仕込まれたトークン
flash[:danger] = "無効なリンクです。"
redirect_to root_url
end
end
#トークンが期限切れかどうか確認する
def check_expiration
if @user.invitation_expired?
flash[:danger] = "招待メールの有効期限が切れています。"
redirect_to root_url
end
end
end
複雑に見えるかも知れないけど、このコードはチュートリアルの12章のpassword_resets_controller.rbとほとんど同じなので理解するのは簡単なはず。
違いは、パスワード再設定の場合は、アクティーベーション済みのユーザーを扱ったけど、招待機能では「未アクティベーション」のユーザーを扱っていることくらいですね。
けど、それもアクティベーションを扱ったチュートリアル11章を理解していれば、そこまで難しくはないはず。
ビューはこんな感じ⬇︎
<% provide(:title, "友人を招待する") %>
<div class="row box invite">
<h3 class="center">友人を招待する</h3>
<div class="col-md-6 col-md-offset-3">
<%= form_for(:invitee, url:invitations_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "招待メールを送信", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<% provide(:title, "Reset password") %>
<div class="row box">
<h3>アカウントを設定する</h3>
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: invitation_path(params[:id])) do |f| %>
<%= render "shared/error_messages", object: f.object %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :name, "名前" %>
<%= f.text_field :name, class: "form-control" %>
<%= f.label :password, "パスワード" %>
<%= f.password_field :password, class: "form-control" %>
<%= f.label :password_confirmation, "パスワードの確認" %>
<%= f.password_field :password_confirmation, class: "form-control" %>
<%= f.submit "登録", class: "btn btn-primary" %>
<% end %>
</div>
</div>
さて、invitations_controllerでは、ユーザーモデルに対するメソッドをふんだんに使ってましたよね。
というわけで、user.rb⬇︎
class User < ApplicationRecord
#:invite_tokenを追加。
attr_accessor :remember_token, :activation_token, :reset_token, :invite_token
#招待メールを送信する
def send_invite_email
UserMailer.invitation(self).deliver_now
end
#ユーザー招待の属性(トークンとダイジェストと、招待したユーザーのid)を作成する。
def create_invite_digest
self.invite_token = User.new_token
update_attributes(invite_digest: User.digest(invite_token), invite_sent_at: Time.zone.now)
end
#招待の期限が切れている場合はtrueを返す
def invitation_expired?
self.invite_sent_at < 24.hours.ago
end
end
招待メールを送信するには、メーラーが必要ですよね?
ということで、メーラーはこちら⬇︎
class UserMailer < ApplicationMailer
def invitation(user)
@user = user
@inviter = User.find(@user.invited_by)
mail to: @user.email, subject: "#{@inviter.name}が01Booksに招待しています。"
end
end
メールの内容も必要ですか?
<h1>01Books</h1>
<p><%= "#{@inviter.name}さんがあなたを01Booksに招待しています。" %></p>
<p>01Booksは、心理学的に正しい本の学習ツールです。</p>
<p>01Booksに参加するには、下のリンクをクリックしてください。</p>
<p> </p>
<%= link_to "01Booksのメンバーになる", edit_invitation_url(@user.invite_token,
email: @user.email) %>
<p> </p>
<p>このリンクは24時間で有効期限が切れます。</p>
<p>
意図せずリンクが切れてしまった場合には、友人にもう一度招待メールを送ってもらうか、お手数ですがTwitterで@kawanji01まで直接DMをください。
</p>
あとはどこか招待ボタンを設置したいところに、<%= link_to "友人を招待する", new_invitation_path %>
てな具合に差し込めば、完成。
ちょっと不親切な説明でしたかね。
でも、GoogleでRails 招待
で検索してもことごとく実装方法が出てこないのだから、きっとみんなdevise_invitableを使っているのでしょう。
車輪の再発明に需要はないんだと思います。いいことです。
けど、ニッチな需要に応えることができるのもWebの長所であると思うので、不親切ながらindexしておきました。
また今回招待機能を再発明する中で実感したのは、なんだかんだチュートリアルを苦しみながらもきちんと学んでおいてよかったということです。
基本さえきちんと理解しておけば、結構応用できちゃうもんなんですね。
苦しめば苦しむほど、学習内容は身につく。
米国の心理学者ロバート・ビョークは、この学習効果を「望ましい困難」と呼んでいます。