Deviseをつかってサインアップ、サインインの実装をしていて、招待機能もつけたいよね。と思ったときにやったことを詳しめに残しておく。
Deviseの実装は一通り終わっていてサインインもサインアップもできるようになってることが前提。既存のサービスに招待機能も、という方が対象にちょっとだけ詳しめに書いた。
環境
Ruby: 2.2.0
Rails: 4.2.0
devise: 3.5.2
devise_invitable: 1.5.5
1. DeviseInvitableをいれる
gem 'devise_invitable'
$ bundle exec rails g devise_invitable:install
insert config/initializers/devise.rb
create config/locales/devise_invitable.en.yml
enしか自動で生成されないので devise_invitable.ja.yml
をつくっておく。
ja:
devise:
failure:
invited: 'アカウントを作成するには、保留中の招待を承認してください。'
invitations:
send_instructions: '招待メールが%{email}に送信されました。'
invitation_token_invalid: '招待コードが不正です。'
updated: 'パスワードが設定されました。お使いのアカウントでログインできます。'
updated_not_active: 'パスワードが設定されました。'
no_invitations_remaining: 'これ以上招待できません。'
invitation_removed: '招待を取り消しました。'
new:
header: '招待する'
submit_button: '招待メールを送る'
edit:
header: 'パスワードを設定する'
submit_button: 'パスワードを設定する'
mailer:
invitation_instructions:
subject: '招待を承認するには'
hello: 'こんにちは、%{email}さん'
someone_invited_you: '%{url}に招待されました。以下のリンクから承認できます。'
accept: '招待を承認する'
accept_until: 'この招待は%{due_date}まで有効です。'
ignore: '招待を承認しない場合は、このメールを無視してください。<br />あなたのアカウントは上記のリンク先にアクセスしパスワードを設定するまでは作成されません。'
time:
formats:
devise:
mailer:
invitation_instructions:
accept_until_format: '%Y年%m月%d日%H時%M分'
2. 招待用のカラムを追加する
Deviseと紐付いているモデルを指定してmigrationを自動生成する。
$ bundle exec rails g devise_invitable User
insert app/models/user.rb
create db/migrate/20160323045302_devise_invitable_add_to_users.rb
usersテーブルにinvitation用のカラムが追加される。:invitations_count
に何人のユーザーを招待したか(招待されて登録した数ではなく、招待メールを送信した人数)が自動でカウントされたり、:invited_by_id
にはどのユーザーによって招待されたかといった情報が自動で入ってくれたりして素晴らしい。
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
$ bundle exec rake db:migrate
3. modelにdevise_invitableのオプションを追記する
:invitable
と invite_for: 24.hours
を新たに加えた。このあたり少しハマりどころだったので後述する。
class User < ActiveRecord::Base
devise :invitable, :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:authentication_keys => [:login], invite_for: 24.hours
end
他にもいろいろ指定できるっぽい。
- invitation_limit: 各ユーザーが招待可能な人数のリミットを指定できる。デフォルトではリミットなし。
- invite_key: 招待を送ったユーザを識別するキーを変更できる。デフォルトではメールアドレスが使われる。
- resend_invitation: 招待メールを再送できるかどうかを変更できる。デフォルトは再送ができる。
- validate_on_invite: 招待メールが送信された段階でユーザーの確認なしにユーザーをつくってしまう。
4. viewをジェネレートコマンドでつくる
view生成用のジェネレートコマンドが用意されているのでそれを使う。viewファイルは app/views/devise
配下にDeviseでつかわれるものと一緒にまとめられる。
$ bundle exec rails g devise_invitable:views
invoke DeviseInvitable::Generators::MailerViewsGenerator
exist app/views/devise/mailer
create app/views/devise/mailer/invitation_instructions.html.erb
invoke form_for
create app/views/devise/invitations
create app/views/devise/invitations/edit.html.erb
create app/views/devise/invitations/new.html.erb
初期状態では招待されたユーザーがアカウントをつくるときに、ユーザー名(:username)を保存できないのでslimで以下のように書き換えた。
h1 Invitations#new
p Find me in app/views/invitations/new.html.erb
= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => {:method => :post} do |f|
= devise_error_messages!
= f.label :email
= f.text_field :email
= f.submit
h1 Invitations#edit
p Find me in app/views/invitations/edit.html.erb
= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => { :method => :put } do |f|
= devise_error_messages!
= f.hidden_field :invitation_token
# :usernameを新しく追加した
= f.label :username
= f.text_field :username
= f.label :password
= f.password_field :password
= f.label :password_confirmation
= f.password_field :password_confirmation
= f.submit
5. controllerをつくる
controllerは自動で生成されないのでつくる。デフォルトの処理から変更したいときはoverrideするだけ。
class Users::InvitationsController < Devise::InvitationsController
def new
super
end
def create
super
end
def edit
super
end
def update
super
end
def destroy
super
end
end
6. :usernameを許可するためにStrong Parameterに変更を加える
application_controllerでStrong Parameterが有効になっている場合にはパーミッションの設定を変更する必要があるので2行追加する。for(:invite)
は招待メールの送信のため、for(:accept_invitation)
は招待されたユーザーがアカウントをつくるために必要。
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) ...
devise_parameter_sanitizer.for(:sign_in) ...
devise_parameter_sanitizer.for(:account_update) ...
# :inviteと:accept_invitationに:usernameを許可する
devise_parameter_sanitizer.for(:invite) { |u| u.permit(:email, :username) }
devise_parameter_sanitizer.for(:accept_invitation) { |u| u.permit(:password, :password_confirmation, :invitation_token, :username) }
# こういう書き方もできるっぽい
devise_parameter_sanitizer.for(:invite) << :username
devise_parameter_sanitizer.for(:accept_invitation) << :username
end
end
7. routingを設定する
Rails.application.routes.draw do
# devise/users
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations',
passwords: 'users/passwords',
# 以下を追記
invitations: 'users/invitations'
}
end
サインインした状態で /users/invitation/new
から招待メールを送信、シークレットウィンドウとかでメールに記載のリンクから招待ユーザーとしてサインアップができるようになってるはず(Deviseで用意されている通常のサインアップとは別フローになる)。
8. ハマったところ
:invite_forを省略した場合は現在時刻が入る
省略したり書き忘れたりした場合は現在時刻(送信時刻)がメール内に入る模様。07時30分に招待メールを送信しているのに、この招待は2016年03月25日07時30分まで有効です。
みたいな感じになってしまう。
:invite_forで指定する時刻はUTC換算なので9時間前が入る
invite_forに指定した期限はUTCで時間換算されてるのでJSTの感覚で書くとだめ。9時間分の時差も含めて指定しないといけない。他に良い方法があれば教えてください...
:invitableと:invite_forは順番変えると動かない
最後までわからなかったのだけれど、この2つは順番が変わると動かなかった。詳しい人がいれば教えてください...
class User < ActiveRecord::Base
# これは大丈夫
devise :invitable, :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:authentication_keys => [:login], invite_for: 24.hours
# これは大丈夫じゃない
devise :invitable, invite_for: 24.hours, :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:authentication_keys => [:login]
# これは大丈夫じゃない
devise :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :omniauthable,
:authentication_keys => [:login],
:invitable, invite_for: 24.hours
end
:invitations_countがカウントアップされない
ユーザーAがユーザーBを招待したとき(ユーザーBがユーザー登録をしたタイミングではなく、招待メールが送信されたとき)、usersテーブルのinvitations_countカラムの値が自動でカウントアップされる仕様になっている。デフォルトでは無効になってるぽいのでdeviseの設定ファイルから以下の行を探して有効にする。
config.invited_by_counter_cache = :invitations_count
ドキュメントも充実しているのでここからいろいろカスタマイズしたい人はこちら。