(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第11章】


前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

 
 認証システム開発・第6段回目、第11章に入ります。セキュリティ強化の観点から、アカウント有効化のステップを入れていきます。よくある、登録後にメールが送られてきて、そのリンクを踏むと本登録になるやつですね。
 
本日のBGMはこちら。
Tatuki Seksu "Hanazawa EP"
いろいろあやしいのをシューゲサウンドでパッケージングしましたってかんじが好き。

 

【11.1.1 AccountActivationsコントローラ 演習】

1. 現時点でテストスイートを実行すると greenになることを確認してみましょう。
→ 現時点ではテスト書いてないのでGREENです。

 
2. 表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
→ pathは相対パス(/とか省略した形)、urlは絶対パス(https:~~とか完全な形)。メールというrails外の処理にはurlの完全形である絶対パスが必要と考えます。

 

【11.1.2 AccountActivationのデータモデル メモと演習】

コールバックではbefore_〇〇の、〇〇の動作の直前に特定のメソッドを実行することができる。メソッド参照という。ブロックを渡すよりこちらが推奨。
(おさらい)privateより下にメソッドを定義することで、外部に非公開にできる。

1. 本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。
→ GREEN

 
2. コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると (Privateメソッドなので) NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
→ こんなかんじ

>> user = User.third
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 2]]
=> #<User id: 3, name: "Mr. Sage Hartmann", email: "[email protected]", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.HyqPb.DwmFICve62DsYte1alLAVihIdeS2F8Rjndry...", remember_digest: nil, admin: false, activation_digest: "$2a$10$9VKv/p9kYrz84SdMs/7s/uzEV3mqzGMmTubIq7.Vz4b...", activated: true, activated_at: "2020-09-17 08:34:09">
>> user.create_activation_digest
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x000000000426b7a0>)
Did you mean?  restore_activation_digest!
>> user.activation_digest
=> "$2a$10$9VKv/p9kYrz84SdMs/7s/uzEV3mqzGMmTubIq7.Vz4bbIb.ZeLDRy"

 
3. リスト 6.34で、メールアドレスの小文字化にはemail.downcase!という (代入せずに済む) メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
→ email.downcase!に変えるだけやね。

user.rb
    def downcase_email
      email.downcase!
    end

 

【11.2.1 アカウント有効化のメール送信 演習】

1. コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?
→ 下記。(クエリパラメータとCGIは用語集に入れてます)

>> CGI.escape('[email protected]')
=> "foo%40example.com"
>> CGI.escape("Don't panic!")            
=> "Don%27t+panic%21"

 

【11.2.2 送信メールのプレビュー メモと演習】

 AWScloud9を使用している場合、チュートリアルでは旧cloud9使用しているので'example.com'に入力する内容の見た目がかなり違うので戸惑いますが、内容は一緒です。Railsサーバーを立ち上げて、別タブで表示した画面のURLのhttps://以下をすべてコピペすればOKです。

1. Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
→ 今日の日付とUTC(協定世界時)が表示される。(協定世界時って言葉カッコよくないですか?中二心をくすぐられる)

 

【11.2.3 送信メールのテスト メモと演習】

 ここのassert_matchがいまひとつしっくり来ないですが、「メールの中身に対し、名前と有効化トークンとエスケープしたメルアドが含まれているかテストしている」ぐらいに認識しておきます。

1. この時点で、テストスイートが greenになっていることを確認してみましょう。
→ Yes, GREEN !

 
2. リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。
→ REDになるのは、メールアドレスにメタ文字が含まれていて、エスケープしないと正規表現ではないからですかね。こちらの記事参照。

 

【11.2.4 ユーザーのcreateアクションを更新 メモと演習】

deliver_now:名前のとおり、その瞬間にメール送信処理を実行。

1. 新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
→ homeに戻りました。Welcome to the Sample App! Click on the link below to activate your account: 以下のURLに含まれているランダムな文字列が有効化トークンです。

 
2. コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
→ activated: false になっていました。

 

【11.3.1 authenticated?メソッドの抽象化 メモと演習】

 もう何のダイジェストの話してんのか混乱してきましたね。ってなったら11章の始めにあった表11.1を見返しましょう。何の話をしているのか、しっかり把握しながら進めないと。
 そんで、この節のタイトルは抽象化より一般化ってかんじがする。authenticated?メソッドを表11.1のどのパターンでも使用できるようにするわけやし。

1. コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
→ こんな感じっすね。ユーザー作っただけだから記憶系はnilのまま…

>> user = User.create(name: "muteki", email: "[email protected]", password: "mutekimuteki", password_confirmation: "mutekimuteki")
   (0.1ms)  SAVEPOINT active_record_1
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
  SQL (2.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "muteki"], ["email", "[email protected]"], ["created_at", "2020-09-17 12:57:50.404544"], ["updated_at", "2020-09-17 12:57:50.404544"], ["password_digest", "$2a$10$eDPAP444JbjJDGucKnoFE.MWFBTAR8dxQ.wXPJfzql9E0TPRVDQfq"], ["activation_digest", "$2a$10$zql927sHRszT.bjitRxBn.slJil.Zvc74AJkztqBZzt7kUiSqBgx."]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 102, name: "muteki", email: "[email protected]", created_at: "2020-09-17 12:57:50", updated_at: "2020-09-17 12:57:50", password_digest: "$2a$10$eDPAP444JbjJDGucKnoFE.MWFBTAR8dxQ.wXPJfzql9...", remember_digest: nil, admin: false, activation_digest: "$2a$10$zql927sHRszT.bjitRxBn.slJil.Zvc74AJkztqBZzt...", activated: false, activated_at: nil>

なので、記憶系をわざわざ作ります。

>> remember_token = User.new_token
=> "5dyf7BoW9H3H9SYH6VPRYg"
>> remember_digest = User.digest(remember_token)
=> "$2a$10$1CymqXEPzP.b05TblQ3Zye/ukhNblEpGlDxI4kT2VoiLUJK1EHVy2"
>> user.update_attribute(:remember_token, remember_token)
   (0.1ms)  SAVEPOINT active_record_1
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> user.update_attribute(:remember_digest, remember_digest)
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-09-17 13:45:51.944577"], ["remember_digest", "$2a$10$1CymqXEPzP.b05TblQ3Zye/ukhNblEpGlDxI4kT2VoiLUJK1EHVy2"], ["id", 102]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
=> true

ということで、各トークンはこんな感じ。ダイジェストは上で出てます。

>> user.remember_token
=> "5dyf7BoW9H3H9SYH6VPRYg"
>> user.activation_token
=> "p5rorOE7trfF4L-YJynnxg"

 
2. リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
→ 確認するだけ…ってあれー??activation_tokenでNameError??
って調べたら解決。あくまでユーザーに紐づいてるものだからuser.activation_tokenか。

>> user.authenticated?(:remember, remember_token)
=> true
>> user.authenticated?(:activation, activation_token)
Traceback (most recent call last):
        1: from (irb):11
NameError (undefined local variable or method `activation_token' for main:Object)

よって以下でtrue。remember_token作るときに、新しく定義するのではなく、user.remember_tokenで紐付けしたほうがよかったな。

>> user.authenticated?(:activation, user.activation_token)
=> true

 

【11.3.2 editアクションで有効化 演習】

1. コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
→ 11.2.4の演習1と同じです。

 
2. 先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。
→ 認証成功、有効化できていました。

 

【11.3.3 有効化のテストとリファクタリング メモと演習】

・テストに出てくる配列deliveriesは変数。なので、他のテストに支障が出ないよう、setupでclearしている。
・sizeメソッド:lengthメソッドと同じ。ここではメールの数(=1)を確認している。
・assignsメソッド:対応するアクション内(ここではsignupをテストしているのでcreateアクション)のインスタンス変数(@user)にアクセスできるようになる。
・whereメソッド:与えられた条件にマッチするレコードをすべて返す。こちらにfind,find_byとの比較がありました。

1. リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう (これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 greenになることも確認してください。
→ 下記

user.rb
  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end

 
2. 現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
→ 下記

users_controller.rb
  def index
    @users = User.where(activated: true).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?
  end

 
3. ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
訳注: updateメソッドは、コールバックとバリデーションを実行せずにスキップしますので、コールバックやバリデーションをかける必要がある場合は注意が必要です。
→ ここ難しいですね。発想としては、「有効化してないユーザー(@non_activated)がindexに表示されていない」のと、有効化してないユーザーのページ(user_path(@non_activated))にアクセスしようとすると、homeに飛ばされる」のを確かめればいいわけだけど…。前者のアサーションが分からず悩みました。
 結局調べたところ、「user_path(@non_activated)のリンクが表示されていない(0である)」のを確かめればいいと。なるほど。ということで下記です。テスト名もてきとうに追記しました。あと、setupに@non_activatedとして、uses.ymlの3番目のユーザーをactivated: falseに書き換えてから追加しています。

users_index_test.rb
  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
    @non_activated = users(:lana)
  end

  test "index as admin including pagination and delete links, 
  not to show non activated user" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.where(activated: true).paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      assert_select 'a[href=?]', user_path(@non_activated), count: 0
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
    get user_path(@non_activated)
    assert_redirected_to root_url
  end

 

【11.4 本番環境でのメール送信 グチと演習】

 ついにきた!!SendGrid!!
これ酷くないですか?登録してもすぐにアカウント凍結されてメール飛ばせなくなるんですよ。前回こいつに時間取られた挙句、結局解決するにはサポートに連絡しないといけないらしく、やってられるかって放置しました。そんなもんチュートリアルに使うなよな…。
 ということで、別の手段を導入しようしていろいろ試したのですがどれもうまくいかず…。不服ですが今回はsendgridで済まします。これは今後の課題として置いておきます。
……結局そっこーで凍結されました。もうええわ。

1. 実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
→ うん、無理でした。

 
2. メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。ヒント: ターミナルからheroku logsコマンドを実行してみましょう。
→ やりたいんやけどね、無理ですわ。

 

第11章まとめ

・終わり悪ければすべて悪い。SendGrid嫌い。
・今後は純粋にActionMailerでメール送信に取り組みたい。
・URLに有効化トークンとエスケープしたメールアドレスを盛り込む。

 
 SendGrid問題、なんとか解決したかったんやけどなあ…。これ以上時間かけるのももったいないので、次に進めます。今後絶対なんとかしてやる!
 さて、次は第12章、パスワードの再設定です。認証システム開発の最終章ですね!

 
⇨ 第12章へ!
⇦ 第10章はこちら
学習にあたっての前提・著者ステータスはこちら

 

なんとなくイメージを掴む用語集

・クエリパラメータ
 URLの末尾で疑問符「?」に続けてキーと値のペアを記述したもの。ユーザーがどこから来たのか解析する手段(パッシブパラメータ)や、指定された変数によってコンテンツの内容を変化させたりできる(アクティブパラメータ)。クエリのこと全般も含めて詳しくはこちら。

・CGI(Common Gateway Interface)
 クライアント側のWebブラウザの要求に応じてWebサーバが外部プログラムを呼び出して、その実行結果がHTTPを介してクライアントのWebブラウザに送信される仕組みのこと。掲示板、アクセスカウンター、アンケートフォームなどを実装できる。

・assert_mutch
 与えられた文字列が与えられた正規表現にマッチした場合、検査にパス。

・SMTP(Simple Mail Transfer Protocol)
 インターネットで電子メールを転送するプロトコル。