20190829のRubyに関する記事は21件です。

Railsアプリでセキュアなパスワードを扱う

Railsアプリでセキュアなパスワードを使用する方法 +α 。

gem追加

Gemfileを修正し、bundle install します。

Railsアプリを rails new で作成後に特に消していなければ、Blowfish暗号を利用できる bcrypt というgemがコメントアウトされている状態になっているはずなので、これをコメントアウトします。

## 省略 ##

- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'

## 省略 ##
$ bundle install

実装

セキュアなパスワードは has_secure_password メソッドを、生成されるmodelファイルで呼び出すことで使用できます。

このメソッドを使うためには、モデル内に password_digest という属性が含まれている必要があるためモデル作成時には注意。

モデル作成

$ rails g model User name:string email:string password_digest:string

modelファイル編集

app/models/user.rb に下記を追加します。

  • before_save ... オブジェクトの保存直前に実行
  • has_secure_password ... 前述
  • validates ... バリデーション処理
class User < ApplicationRecord

  before_save { email.downcase! }

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
  # 半角英数字大文字小文字をそれぞれ1文字以上含む
  VALID_PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,16}+\z/.freeze

  validates :name,
            presence: true,
            length: { in: 1..30 }

  validates :email,
            presence: true,
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: { case_sensitive: false }

  has_secure_password
  validates :password,
            presence: true,
            length: { minimum: 8 },
            format: { with: VALID_PASSWORD_REGEX }
end

Migration

$ rails db:migrate
      invoke  active_record
      create    db/migrate/20190829085116_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

確認

User.new で作成したオブジェクトを save メソッドで保存。

ハッシュ化されたパスワードが登録されていることを確認します。

$ rails c
Loading development environment (Rails 5.2.3)

irb(main):001:0> user = User.new(name: "kohbis", email: "tEst@test.com", password: "Passw0rd")
=> #<User id: nil, name: "kohbis", email: "tEst@test.com", password_digest: "$2a$12$pPsDOoJErpVXsdtLWNoGvOTdmUFBSGk/nf492aA0N.1...", created_at: nil, updated_at: nil>

irb(main):002:0> user.save
   (0.9ms)  BEGIN
  User Exists (1.3ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = 'tEst@test.com' LIMIT 1
  User Create (0.5ms)  INSERT INTO `users` (`name`, `email`, `password_digest`, `created_at`, `updated_at`) VALUES ('kohbis', 'test@test.com', '$2a$12$pPsDOoJErpVXsdtLWNoGvOTdmUFBSGk/nf492aA0N.18p.sIGQ/Ce', '2019-08-29 12:39:14', '2019-08-29 12:39:14')
   (2.2ms)  COMMIT
=> true

irb(main):003:0> User.all
  User Load (0.9ms)  SELECT  `users`.* FROM `users` LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "kohbis", email: "test@test.com", password_digest: "$2a$12$pPsDOoJErpVXsdtLWNoGvOTdmUFBSGk/nf492aA0N.1...", created_at: "2019-08-29 12:39:14", updated_at: "2019-08-29 12:39:14">]>

最後にvalidationが効いているかも確認しておきます。

$ rails c
Loading development environment (Rails 5.2.3)
irb(main):001:0> user = User.new(name: "", email: "@not.address", password: "password")
   (0.6ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<User id: nil, name: "", email: "@not.address", password_digest: "$2a$12$5PovSBWfHCPDOKQcD3lCAuOC7YepZvIwIaocF01tEI5...", created_at: nil, updated_at: nil>

irb(main):002:0> user.valid?
  User Exists (0.5ms)  SELECT  1 AS one FROM `users` WHERE `users`.`email` = '@not.address' LIMIT 1
=> false

irb(main):003:0> user.errors.messages
=> {:name=>["can't be blank", "is too short (minimum is 1 character)"], :email=>["is invalid"], :password=>["is invalid"]}

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fishでrbenvを使う方法

bashからfishに乗り換えたのだけれども、Rubyのバージョンが上手く切り替わらなかったのでメモ

最初のRubyバージョン

 ~/D/G/rails_demo    ruby -v                                    Thu Aug 29 22:37:17 2019
ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]

rbenv initを一回行う

 ~/D/G/rails_demo    rbenv init                                 Thu Aug 29 22:38:56 2019
# Load rbenv automatically by appending
# the following to ~/.config/fish/config.fish:

status --is-interactive; and source (rbenv init -|psub)

念の為以下コマンドを実行する

~/D/G/rails_demo     status --is-interactive; and source (rbenv init -|psub)

ruby -v を実行してバージョンが切り替わっていたらOK

 !  ~/D/G/rails_demo    ruby -v                                Thu Aug 29 22:38:57 2019
ruby 2.3.7p456 (2018-03-28 revision 63024) [universal.x86_64-darwin18]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

is_a?(Arry)メソッド

is_a?(Arry)メソッド

レシーバのオブジェクトが引数クラスのインスタンスであればtrue、そうでなければfalseを返します。
この場合だとarryならtrueを返す
https://ref.xaio.jp/ruby/classes/object/kind_of

コンソールで試した
[1] pry(main)> arr = [1,2,3]
=> [1, 2, 3]

[2] pry(main)> arr.is_a?(Array)
=> true

[3] pry(main)> arr.is_a?(Hash)
=> false

[4] pry(main)> b = {unko: 3}
=> {:unko=>3}

[5] pry(main)> b.is_a?(Hash)
=> true

[6] pry(main)> b.is_a?(Array)
=> false

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オススメ! Evryday Rails - RSpecによるRailsテスト入門を購入しました!

タイトルの通りEvryday Rails - RSpecによるRailsテスト入門を購入しました。
購入しようと思った理由は、RSpecを書いてみたくて、いままで自分で調べながらやっていたけど、情報がバラバラで自分ではいまいち理解できずモヤモヤしていたから。

今日は3章までやってみたが、とにかく分かりやすい!モヤモヤしてたのがス~と消えてく感じです。これから頑張っていきます!

余談

Windows 10 Home
購入してさっそく勉強を始めようとして、Google Chromeをダブルクリックしたらなぜか開かない:scream:
昨日まで正常に動いたのに。Microsoft Edgeを開いたらこれは正常に起動した。調べていろいろ試したけど変化が無かったので、結局Chromeを削除し、再インストールしたら起動するようになりました。なぜ急に壊れた???

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #13 パスワード再設定編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#12 ActionMailer, アクティベーション編
次回:準備中

こんなことが分かる

  • パスワードを再設定させる方法
  • トークンとダイジェストを生成、認証する方法
  • メーラーの使い方
  • RSpecでコントローラのインスタンス変数を用いる方法

今回の流れ

  1. パスワード再設定のイメージをつかむ
  2. ビューを作る
  3. トークンとダイジェストを生成しURL入りメールを送信する
  4. URLの情報が正しいか確認し再設定する

パスワード再設定のイメージ

パスワード再設定は#12で紹介したアクティベーションと似ています。
ぜひ比較しながらご覧ください。
lantern_lantern_reset_password_sitemap.png
アクティベーションの時はビューが必要ありませんでした。
しかし今回は2つのビュー4つのアクションが必要です。

今回も先にコントローラとリソースを生成します。

bash
$ rails g controller PasswordResets new create edit update
config/routes.rb
# 中略
resources :password_resets, only: [:new, :create, :edit, :update]

パスワード再設定までをつくる手順

以下はパスワード再設定をつくるときの手順です。

  1. パスワード再設定用のビューを作る(new)
  2. トークンとダイジェストを生成し、メールを送信する(create)
  3. メール内のURLにトークンとメールアドレスを忍ばせる
  4. URLをクリックしたらURL内の情報が有効か確認する(edit)
  5. 確認した情報や入力したパスワードが正しければ再設定が完了する(update)

パスワード再設定用のビューを生成(new)

それでは始めていきましょう。
始めに先にビューを完成させてしまいます。
整えるビューは3つです。

  • ログイン画面にパスワード再設定リンク(メール入力)を追加する
  • パスワード再設定画面(メール入力)を作る
  • パスワード再設定画面(パスワード入力)を作る

ログイン画面にリンクを追加

ログイン画面のビューからパスワード再設定にアクセスできるように編集します。

app/views/sessions/new.html.erb
<% provide(:title, "ログイン") %>
<div class="container form-container login-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">ログイン</h1>
      <%= form_with(scope: :session, url: login_path, local: true) do |form| %>

        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>
        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>
        <div class="form-group form-check">
          <%= form.check_box :remember_me, class: 'form-check-input' %>
          <%= form.label :remember_me, '次から保存(ログイン省略)', class: 'form-check-label' %>
        </div>
        <div class="form-group">
          <%= form.submit "ログイン", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
      <p class="form-go-to-signup-or-login">新しくはじめる方は<%= link_to "こちら", signup_path %></p>
      <p class="form-go-to-password-reset">パスワードを忘れた方は<%= link_to "こちら", new_password_reset_path %></p>
    </div>
  </div>
</div>

lantern_lantern_login_with_reset_password.png

パスワード再設定画面(メール入力)を作る

続いてメールを送るまでのパスワード再設定画面を作ります。

app/views/password_resets/new.html.erb
<% provide(:title, "パスワード再設定依頼") %>
<div class="container form-container password_reset-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">パスワード再設定</h1>
      <%= form_with(scope: :password_reset, url: password_resets_path, local: true) do |form| %>
        <div class="form-group">
          <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
        </div>
        <div class="form-group">
          <%= form.submit "送信", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
      <p class="form-go-to-signup-or-login">思い出した方は<%= link_to "こちら", login_path %></p>
    </div>
  </div>
</div>

lantern_lantern_reset_password_1.png

パスワード再設定画面(パスワード入力)を作る

ここはちょっとクセがあります。
なぜならPATCHであるapdateアクションは、何をキーにしてユーザを判別すれば良いのか考える必要があるからです。

メール送信時(GET / #edit)はURL内のメールアドレスをキーにしました。
同じようにするにはフォームでメールアドレスを送信してほしいものです。
しかしユーザ側からすると手間になります。

そこで隠しフィールドを使います。

<%= hidden_field_tag :email, @user.email %>

こんな風に記述すると見えない形でパラメータを送ってくれます。
(params[:email]の形で取得できます)
これを用いつつ、ビューを完成させましょう。

app/views/password_resets/edit.html.erb
<% provide(:title, "パスワード再設定") %>
<div class="container form-container password_reset-container">
  <div class="row">
    <div class="col">
      <div class="form-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="form-title">パスワード再設定</h1>
      <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |form| %>
        <%= render 'shared/error_messages' %>

        <%= hidden_field_tag :email, @user.email %>
        <div class="form-group">
          <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
        </div>
        <div class="form-group">
          <%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード(再入力)" %>
        </div>
        <div class="form-group">
          <%= form.submit "送信", class: 'btn btn-info btn-lg form-submit' %>
        </div>
      <% end %>
    </div>
  </div>
</div>

lantern_lantern_reset_password_2.png
これでビューは完了しました。

トークンとダイジェストを生成しメールを送信する(create)

続いてはトークンとダイジェストの生成からメール送信までを記述しましょう。
手順は以下の通りです。

  • ダイジェスト用の属性を与える
  • トークンとダイジェストを生成、認証するメソッドを確認する
  • 再設定用トークンとダイジェストを生成、メールを送信するメソッドをつくる

ダイジェスト用の属性を与える

その前にマイグレーションで属性を与えます。
以前の記事と異なるのは、再設定の有効期限を記す属性も加えるという点です。

bash
$ rails g migration add_reset_to_users reset_digest:string reset_sent_at:datetime
$ rails db:migrate

トークンとダイジェストを生成、認証するメソッドを確認する

トークンやダイジェストの生成、認証するメソッドは#9ですでに紹介しています。
すでに#9をお読みの方は飛ばしてください。
それ以外の方は以下のメソッドを追加してください。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  class << self
    # ダイジェストを生成する
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end

    # トークンを生成する    
    def new_token
      SecureRandom.urlsafe_base64
    end
  end

  # トークンとダイジェストを認証する
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

再設定用トークンとダイジェストを生成、メールを送信するメソッドをつくる

次はメール送信までに必要なメソッドを用意しましょう。
必要なメソッドを列挙します。

  • create_reset_digest → トークンとダイジェストをまとめて生成
  • send_password_reset_email → メールを送信

トークンは仮属性なのでattr_accessorによる記述もお忘れないように。

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  # 中略
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
  end

  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  private
  # 中略
end

ついcreate_reset_digestをcreate_activation_digestのすぐ下に記述したくなります。
しかしここはprivateメソッドです。
なぜprivateを使い分けているのでしょうか。

理由はこうです。

  • create_activation_digestはこのUserクラス内でしか使いません
  • よって余計なスコープを広げないために、privateメソッドを使用します
  • でもcreate_reset_digestはコントローラで使用します
  • よってcreate_reset_digestはUserクラスを超えるのでprivateには置けません

使い分けられるよう、心がけましょう。

createアクションからメール送信などを行う

それではcreate_reset_digestなどをコントローラで使いましょう。
メール送信に関する部分はcreateアクションに記述します。

app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "再設定用のURLを入力したメールに送信しました"
      redirect_to root_url
    else
      flash[:danger] = "お使いのメールアドレスは登録されていません"
      render 'new'
    end
  end

  def edit
  end

  def update
  end
end

メールの設定を行う

createアクションでメール送信を行いました。
続いてはメール内容を整えます。

メーラーの生成に関しては#12でご確認ください。

お読みでない方用に、必要なコードのみ紹介します。
#12をお読みの方はこのコードのみ飛ばしてください

bash
$ rails g mailer UserMailer account_activation password_reset
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'noreply@example.com'
  layout 'mailer'
end

本番環境(〇〇は本番環境で自分のアプリを開いたときのURL)

config/environments/production.rb
# 中略
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '〇〇.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
  }

この後からはお読みの方も必要な手順です。
間違えないよう注意していただけたら幸いです。

メールの送信先とタイトルをつくる

送信先とタイトルはUserMailerクラスの仕事です。
記述しましょう。

mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  # 中略
  def password_reset(user)
    @user = user
    mail to: user.email, subject: "【重要】Lantern Lanternよりパスワード再設定のためのメールを届けました"
  end
end

メール内のURLにトークンとメールアドレスを忍ばせる

メールの本文はViewの仕事です。
Viewはhtml版とtext版で2つあるので注意しましょう。
こんな風に記述するとURLにトークンとメールアドレスを仕込むことができます。

app/views/user_mailer/password_reset.html.erb
<h1>Lantern Lantern</h1>
<p><%= @user.name %>さんへ</p>
<p>下記の『パスワードを再設定する』をクリックしてパスワードを再設定してください。</p>
<%= link_to "パスワードを再設定する", edit_password_reset_url(@user.reset_token, email: @user.email) %>
app/views/user_mailer/password_reset.text.erb
<%= @user.name %>さんへ

Lantern Lanternです。下記のリンクからパスワードを再設定してください。
<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

どうやってURLを指定したのか。
#12でも説明しましたがこちらでも解説します。
URLの指定にはこのようなコードを使用しました。

edit_password_reset_url(@user.reset_token, email: @user.email)

まずはルートを調べてURLのパターンを確認しましょう。

bash
$ rails routes | grep edit_password_reset
edit_password_reset GET /password_resets/:id/edit(.:format)

『:id』と書かれたこの部分。
ここがedit_password_reset_urlの第1引数に対応します。
だからここにトークンを記述しています。

では第2引数は何をしているのでしょう。
第2引数はハッシュを使用するとクエリパラメータを付与します。
具体的には最後(/editの直後)にemailキーとメールアドレスを組み込みます。

結果としてこんなURLが生成されます。

http://www.example.com/password_resets/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

以上がURLのからくりです。

メールのプレビューをつくる

先ほど作ったメールがどんな感じに作られたか。
確認するためにプレビューが欲しいところです。
まずはspec内にこのような記述をしましょう。

spec/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
  # 中略
  # https://〇〇/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end
end

加えてテスト環境の設定が必要です。
(#12を済ませた方は必要ありません)
#12にも書いていますが、一応ここでも簡易的に紹介します。

config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = '〇〇'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  config.action_mailer.perform_caching = false

'〇〇'のところは自分の環境によって変更する必要があります。
具体的にはdevelopment環境のURLを挿入します。
(わからない方は#12をご覧ください)

これが終わると以下のURLにアクセスしましょう。
するとプレビューが表示されます。

https://〇〇/rails/mailers/user_mailer/password_reset

lantern_lantern_passowrd_reset_preview.png
以上でプレビューは完了です。

情報が正しいか確認し再設定する(edit, update)

URLがクリックされたらeditアクションを呼び出します。
ここで行うことは3つあります。

  • Userモデルを特定する
  • 存在するか、トークンは正しいのかを確かめる
  • URLが期限切れでないかを確かめる

この3つに関してはupdateアクションでも同じことを行います。
ということはこうした方がスッキリします。

  • 上記3つをメソッド化する
  • editとupdate時に呼び出す

アクションの直前にメソッドを呼び出すにはbefore_actionを使います。

一方updateアクションはフォームにパスワードが入力された時の処理を書きます。
今回は3つのケースに対応します。

  1. 入力されていない時
  2. 無効なパスワードの時
  3. 正しい時

こちらはupdateアクションのみの振る舞いなので、直接書き込みます。
では、これらを踏まえて実装しましょう。

app/controllers/password_resets_controller.rb
class PasswordResetsController < 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
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "再設定用のURLを入力したメールに送信しました"
      redirect_to root_url
    else
      flash[:danger] = "お使いのメールアドレスは登録されていません"
      render 'new'
    end
  end

  def edit
  end

  def update
    if params[:user][:password].empty?
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      @user.update_attribute(:reset_digest, nil)
      flash[:success] = "パスワードの再設定が完了しました"
      redirect_to @user
    else
      render 'edit'
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end

    def get_user
      @user = User.find_by(email: params[:email])
    end

    def valid_user
      unless @user && @user.activated? && @user.authenticated?(:reset, params[:id])
        flash[:danger] = "無効なURLです。再度メールアドレスを入力してください"
        redirect_to new_password_reset_url
      end
    end

    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "パスワード再設定URLの有効期限が過ぎています。再度メールアドレスを入力してください"
        redirect_to new_password_reset_url
      end
    end
end

1つ言い忘れたことがあります。
それはcheck_expiration内にあるpassword_reset_expired?メソッドです。

期限切れかどうかを判別する処理に関しては、別途Userモデルにメソッドを用意した上で実装しています。

そちらのメソッドを紹介します。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  private
    # 中略

これでパスワード再設定に関する全ての実装が完了しました。

テストを書く

最後にパスワード再設定に関するテストを完成させます。
いくつかあるので順に見ていきます。

メーラーテストを書く

まずはメーラーのテストです。
メールの内容についてテストします。
なおメール本文に関してはデコードを行って検証しています。

spec/mailers/user_mailer_spec.rb
require "rails_helper"

RSpec.describe UserMailer, type: :mailer do

  let(:user) { create(:user) }

  # 中略
  describe "password_reset" do
    it "renders mails" do
      user.reset_token = User.new_token
      mail = UserMailer.password_reset(user)
      expect(mail.subject).to eq "【重要】Lantern Lanternよりパスワード再設定のためのメールを届けました"
      expect(mail.to).to eq ["michael@example.com"]
      expect(mail.from).to eq ["noreply@example.com"]
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include "Michael Example"
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include user.reset_token
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include CGI.escape(user.email)
    end
  end
end

パスワード変更のテストを書く

メーラー以外のテストを書きます。
まずはRequest specを生成するところから始めましょう。

bash
$ rails g rspec:request password_resets

今回テストする内容は以下の通りです。

createアクション

  • 無効なメールアドレス
  • 有効なメールアドレス

editアクション

  • 無効なメールアドレス
  • 無効なユーザ
  • 無効なトークン
  • 有効なメールアドレス、ユーザ、トークン

updateアクション

  • 無効なパスワード
  • 空のパスワード
  • 期限切れのトークン
  • 有効なパスワード

では記述します。

spec/requests/password_resets_spec.rb
require 'rails_helper'

RSpec.describe "PasswordResets", type: :request do

  let(:user) { create(:user) }

  describe "POST /password_resets" do
    it "is invalid email address" do
      get new_password_reset_path
      expect(request.fullpath).to eq '/password_resets/new'
      post password_resets_path, params: { password_reset: { email: "" } }
      expect(flash[:danger]).to be_truthy
      expect(request.fullpath).to eq '/password_resets'
    end

    it "is valid email address" do
      get new_password_reset_path
      expect(request.fullpath).to eq '/password_resets/new'
      post password_resets_path, params: { password_reset: { email: user.email } }
      expect(flash[:info]).to be_truthy
      expect(flash[:danger]).to be_falsey
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end
  end

  describe "GET /password_resets/:id/edit" do
    it "is invalid email address" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: "")
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end


    it "is invalid user" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      user.toggle!(:activated)
      get edit_password_reset_path(user.reset_token, email: user.email)
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
      user.toggle!(:activated)
    end

    it "is invalid token" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path('wrong token', email: user.email)
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end

    it "is valid information" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      expect(flash[:danger]).to be_falsey
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}/edit?email=#{CGI.escape(user.email)}"
    end
  end

  describe "PATCH /password_resets/:id" do
    it "is invalid password" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "barquux"
        }
      }
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}"
    end

    it "is empty password" do
    post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "",
          password_confirmation: ""
        }
      }
      expect(request.fullpath).to eq "/password_resets/#{user.reset_token}"
    end

    it "has expired token" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      user.update_attribute(:reset_sent_at, 3.hours.ago)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "foobaz"
        }
      }
      expect(flash[:danger]).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/password_resets/new'
    end

    it "is valid information" do
      post password_resets_path, params: { password_reset: { email: user.email } }
      user = assigns(:user)
      get edit_password_reset_path(user.reset_token, email: user.email)
      patch password_reset_path(user.reset_token), params: {
        email: user.email,
        user: {
          password: "foobaz",
          password_confirmation: "foobaz"
        }
      }
      expect(flash[:success]).to be_truthy
      expect(is_logged_in?).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq "/users/1"
    end
  end
end

このテストではassignsを使用するところがあります。

# 該当箇所
user = assigns(:user)
get edit_password_reset_path(user.reset_token, email: "")

これによりFactoryBotのuserではなく、コントローラ内インスタンス変数のuseruserとして取得し直しています。なぜでしょう。

理由は以下の通りです。

  • edit_password_reset_pathの引数に当たるreset_tokenは、attr_accessorによって生成された仮属性です。
  • 仮属性なのでletで生成したFactoryBotのuserにはreset_tokenが存在しません。よってエラーになります。
  • したがってreset_tokenが代入されたPasswordResetsコントローラのインスタンス変数userを使用する必要があります。

というわけで、コントローラのインスタンス変数のuserが必要です。
そこでassignsを使用します。
assignsはコントローラのインスタンス変数を取得します。

そのためにはgem 'rails-controller-testing'が必要です。
(とてもためらいましたが)導入しましょう。

Gemfile
group :development, :test do
+ gem 'rails-controller-testing'
end
bash
$ bundle install

これで問題なく動作します。
テストを走らせてみましょう。

bash
$ rails spec

問題なければ、以上でテストは終了です。

追記:ですます調に統一しました

今回から語尾を「ですます」に統一しました。
情報が統一されて見やすいかなと。
分かりやすい記事になるよう努めます。

やさしい記事の書き方↓
これであなたのQiita記事もランキング入り!?@jnchitoによる編集リクエスト解説(解説動画付き)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【TwitterAPI】「贅沢な名だねえ。今からお前の名前は○○だ。」【Ruby】

動機

  • 2019/8/16に金曜ロードショーで「千と千尋の神隠し」が放送された
  • 前に少し流行ったupdate_name(ツイッター上で、リプライに反応して自分のTwitterネームを変更するもの)がUserStreamの廃止に伴い実装が困難になっていて、どうにかREST APIだけで再現できないか考えてた
  • この記事を見て、似たようなものをRubyで書いてみようと思ってた

みんな僕のTwitterの名前を自由に変えていいよ

環境と使ったもの

  • macOS 10.14.5
  • Ruby 2.5.1
  • TwitterAPI … Twitter Developersに登録済みであることが前提です。

仕様

  1. 「いいかい、今から@fyhcu の名前は○○だ。」というリプライを、最新50件のリプライから正規表現で見つける。台詞は少し変えてます。(できるだけ簡単な文章にしたかったので)
  2. Twitterの制限上、名前が50文字以上になるものは設定できないのでこれを除外。さらに、以前に候補に上がったものも除外。候補になったかどうかはいいねしているかどうかで判断します。
  3. 候補となったツイートを配列に格納し、そのツイートをいいねする。(これで次動いた時の候補に挙がらなくなります)
  4. 格納した候補からランダムに一つ選ぶ。
  5. 名前をそのワードに変更。変更したことをツイート。

結果

IMG_6494.jpg

IMG_6496.jpg

実装

メイン部分

main.rb
require_relative '../Privatekey/oauth_twitter'
require_relative 'isfavorited'
require 'timers' #定期実行用

timers = Timers::Group.new

timers.every(600){ #600秒 = 10分ごとに処理

  reply_array = [] #リプライを格納する配列
  random = 0 #改名候補ツイートから無作為に選ぶため

  reg1 = /^いいかい、今から@fyhcu の名前は(.*)だ。$/ #湯婆婆

  @client.mentions(count: 60).each do |reply|
    if (reg1 =~ reply.text) #湯婆婆になっているリプライを探す
      puts "-----------------------------------------------------------"
      puts reply.text

      #名前が50文字以上に設定されているツイートは除外
      if ($1.length >= 50)
        next #次の繰り返しに移動
      end

      #既に自分がふぁぼってるツイートは除外
      if isfavorited(reply.id)  == true
        puts "<<<<<<<この返信は既にいいねされています>>>>>>>>"
        next
      end

      reply_array << $1 #改名候補を格納

      @client.favorite(reply.id) #ツイートをいいね

    end
  end

  puts "-----------------------------------------------------------"
  time_f = Time.now.strftime("%F %X") #実行時間を記録

  if reply_array.size > 0 #改名候補がある時のみ

    random = rand(reply_array.size) #ランダムに数字を一つ選ぶ
    aftername = reply_array[random] #変更後の名前を決定
    puts "#{aftername}に改名決定です"

    @client.update_profile(:name => aftername) #改名

    tweet = "湯婆婆によって名前が#{aftername}に変えられましたた。"
    @client.update tweet

    #ログに書き込み
    File.open("../log/yubaba-app-log.txt", "a") do |text|
      text.puts(" => #{aftername}に改名 [#{time_f}]")
    end

  else #改名候補がない時(配列に何も格納されていない時)

    puts "改名候補がありませんでした。"

    #ログに書き込み
    File.open("../log/yubaba-app-log.txt", "a") do |text|   
      text.puts(" => 変化なし [#{time_f}]")
    end
  end
}

1000.times{ #1000回実行(この実装は良くなさそう)
  timers.wait
  puts Time.now.strftime("****************** %Rに実行しました ******************")
 }

湯婆婆構文は正規表現を使って拾っています。正規表現で一致し、かつ特定の条件を満たしたリプライの変更後の名前部分を配列に格納しています。正規表現のグルーピング()と後方参照$1,$2,...がとても役に立ちました。次以降の候補に入らないように、@client.favorite(id)でいいねしています。UserStreamが動いていたあの頃のupdate_nameに近付けたいので、10分毎に定期実行をかけています。Timerを使っていますが、多分良くない気がするので近いうちにHeroku Schedulerとか使えたらなと思います。

また、main.rbを実行すると、ターミナル上に正規表現によって拾われたツイートとそれがいいねされているかどうかが表示されます。実行時に改名の有無にかかわらずログを書くようにもしました。

※このコードのままだと一定時間内に2回同じ名前に変更されると2回目以降改名した旨のツイートか重複によりツイートできない状況になってしまいます。例外処理でなんとかできれば…。(気が向いたら実装します)

isfavoriteメソッド

「自分があるツイートに対していいねしているか」をbooleanで返すメソッドがなかったので自作しました。JSON.parseが便利でした。

isfavorited.rb
require_relative '../Privatekey/oauth_twitter'
require 'json' #JSON扱うためのgem
require 'oauth'

# ツイートに対して、自分がいいねしているか調べるメソッド

def isfavorited(tweet_id)
  consumer = OAuth::Consumer.new(
    @client.consumer_key,
    @client.consumer_secret,
  )

  endpoint = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
  responce = endpoint.get("https://api.twitter.com/1.1/statuses/show/#{tweet_id.to_s}.json")
  result = JSON.parse(responce.body)
  fav = result["favorited"]

end

https://api.twitter.com/1.1/statuses/show/#{tweet_id.to_s}.json" は、ツイート(ID指定)の個別情報を取得できるエンドポイントで、レスポンスはJSONです。

GET statuses/show/:id - TwitterDevelopers

レスポンスボディの中にあるfavorited: true(false)が自身がいいねしたかどうかを示しています。これを引っ張り出してきて最終的にbooleanをそのまま返すようにしてます。実際、この2行があるだけで結構いろんなことができるなあと思っています。

responce = endpoint.get("https://api.twitter.com/1.1/statuses/show/#{tweet_id.to_s}.json")
result = JSON.parse(responce.body)

感想

意外と簡単に実装できました。いっぱい遊んでもらえたので満足です。これからもくだらないものを実装してみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでbundle installの権限エラーが出た

はじめに

こんにちは^^
こちらの記事では、Railsのコマンド入力でbundle installをした場合に、

Errno::EACCES: Permission denied @ rb_sysopen...
Make sure that `gem install mysql2 -v '0.4.10' --source 'https://rubygems.org/'` succeeds before bundling.

などのエラーが出て、上記のgem install mysql2...を入力し、再度bundle installをすると、また別のgem installコマンドを入力してくださいと何度も何度も何度も・・・

何度gem installとbundle installをすれば良いんだという。。。
これを一度のbundle installコマンドで手っ取り早くインストールを可能にする方法をご紹介します。

前提:Mac, rbenvでRailsインストール

原因は、、、?

そもそも一度のbundle installでgemが一気にインストールされないのは、rbenvの所有者が異なることが原因です。

$ ls -la /Users/ユーザー名/.rbenv/versions/2.6.2

などとコマンドを打ってみる(Macの場合)と所有者がrootになっていると思います。ですので、所有者を現ログインユーザーに変更すれば一気にインストールできるかと思います^^
ls -laコマンドは指定したファイルやフォルダの詳細情報を見ることができるコマンドです。
よく聞くrwxの権限が見れたり所有者が見れたり、サイズが見れたり作成日が見れたりもします。

コマンド入力

所有者をコマンドによって変更したいと思います。(所有者を変更すると環境によっては不具合が出ることがあるかと思いますので、変更する際はしっかりとご確認ください。)
chownは所有者を変更するコマンドで、-Rは再帰(下の方にある構造にも所有者変更を反映させる)を設定するものになります。
Railsのインストールにrbenvを使用していることが前提となってしまいますが、ここの所有者を変更することが大事になります^^
Ralisをインストールした時のパスは適宜変更していただければと思います。

sudo chown -R ユーザー名 /Users/ユーザー名/.rbenv/versions/2.4.2

終わりに

以上、Railsでbundle installの権限エラーが出た場合の対処法をご紹介しました。
プログラミングを勉強をしているのに、コマンドでつまづくのは嫌、、、なんでここで。。。という風に思うことは多々あります。コマンドラインの知識はいずれ必要になってくるものなので、今は一旦意味わからずコマンドを打っても致し方ないと思いますが、あとでしっかり勉強しましょう!
もちろん業務で意味わからないコマンドを打つのは厳禁です。。。
よくつい打ってしまったというコマンドでたまに聞くのはrm -rf /*とかrm -rf .とかですかね、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

change_column の rollback エラー

migration の change メソッドは change_column をサポートしてない。なので、rollback した時にエラーになる。change_column を使いたいときは、明示的に up メソッドと down メソッドを使う必要がある。

class ChangeColumnToUser< ActiveRecord::Migration

  # 変更内容(db:migrate時に実行される)
  def up
    change_column :users, :name, :string #nameカラムのデータ型をstringへ変更
  end

  # 変更前の状態(db:rollback時に実行される)
  def down
    change_column :users, :name, :text
  end
end

changeメソッドがサポートしているメソッドは以下。

add_column
add_foreign_key
add_index
add_reference
add_timestamps
change_column_default (:fromと:toの指定は省略できない)
change_column_null
create_join_table
create_table
disable_extension
drop_join_table
drop_table (ブロックを渡さなければならない)
enable_extension
remove_column(型を指定しなければならない)
remove_foreign_key(2番目のテーブルを指定しなければならない)
remove_index
remove_reference
remove_timestamps
rename_column
rename_index
rename_table

sqlが絡んだりする複雑なマイグレーションを書くときは reversible を使う。

class ExampleMigration < ActiveRecord::Migration[5.0]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end

    reversible do |dir|
      dir.up do
        # CHECK制約を追加
        execute <<-SQL
          ALTER TABLE distributors
            ADD CONSTRAINT zipchk
              CHECK (char_length(zipcode) = 5) NO INHERIT;
        SQL
      end

      dir.down do
        execute <<-SQL
          ALTER TABLE distributors
            DROP CONSTRAINT zipchk
        SQL
      end
    end

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end
end

source: https://railsguides.jp/active_record_migrations.html#change%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E4%BD%BF%E3%81%86

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseでログイン機能を実装する

Railsのgemであるdeviseについて、導入からログイン機能の実装までを投稿します。

deviseとは

ユーザーの新規登録、ログイン、ログアウトなど、認証に必要な機能を追加することができるgemです。

deviseのインストール

Gemfileにdeviseを追加

gem 'devise'

gemをインストールします。

$ bundle install

以下のコマンドで、関連ファイルをインストールします。

$ rails g devise:install

これにより、config/initializers/devise.rb、create config/locales/devise.en.ymlというファイルが作成されます。

deviseの設定

まず、config/environments/development.rbに、URLを追記します。

config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

これは、ユーザーの新規登録などで使われる、認証メールに記載されるURLを設定しています。なので、メールによる認証を行わない場合はスキップして構いません。

また、エラーメッセージを表示させるために、app/views/layouts/application.html.erbに追記します。

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

これを、<%= yield %>の直前に追記します。

ユーザーモデルの作成

認証用のユーザーモデルを作成するには、通常の「rails g model モデル名」ではなく、「rails g devise モデル名」を使います。

$ rails g devise user

作成されたユーザーモデルは、以下のとおりです。

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

deviseの中に記載されているものは、モジュールと呼ばれるもので、それぞれ以下のような機能があります。

・database_authenticatable  DBに保存されるパスワードが正しいかの検証と暗号化
・registerable  サインアップ処理
・recoverable  パスワードのリセット
・rememberable  クッキーにログイン情報を保持
・trackable  サインイン回数・時刻・IPアドレスを保存
・validatable  メールアドレスとパスワードのバリデーション
・confirmable  メール送信による登録確認
・lockable  一定回数ログインに失敗した際のアカウントロック
・timeoutable  一定時間でセッションを削除する
・omniauthable  OmniAuthサポート

デフォルトでは、すべてのモジュールが設定されているわけではないので、必要に応じて追記しましょう。

また、作成されたマイグレーションファイルについても見ていきます。

db/migrate/xxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration
  def change
    create_table(:users) do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

デフォルトでコメントアウトされているモジュールを、上のapp/models/user.rbで追加した場合、マイグレーションファイルのコメントも外すようにしましょう。

モジュールの編集が終わったら、マイグレーションを実行し、テーブルを作成します。

$ rails db:migrate

この時点で、/users/sign_upにアクセスし、Sign upの画面が表示されれば無事成功です。

あとは、必要に応じて、ビューを編集していきましょう。

ビューの作成

$ rails g devise:views

これにより、モデルに対応するいくつかのビューが作成されます。registrations/new.html.erb(新規登録画面)、sessions/new.html.erb(ログイン画面)、registrations/edit.html.erb(ユーザー編集画面)など、必要に応じてレイアウトを編集しましょう。

コントローラの作成

$ rails g devise:controllers users

コントローラを編集したい場合、このコマンドを実行します。また、ルーティングの設定もしましょう。例えば、registrations_controller.rb、sessions_controller.rbを使う場合は以下のようにします。

config/routes.rb
devise_for :users, controllers: {
  registrations: 'users/registrations',
  sessions:      'users/sessions',
}

参考サイト

https://qiita.com/ShinyaKato/items/a098a741a142616a753e

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails6/windows10/ubuntu/mySQL

windows10でruby on rails使えるようにします。

まずubuntuを入れます。(省略)
rbenvでRubyをインストールします。(省略)
node.jsとかBundlerを入れます。(省略)

# mkdir rails_app
# cd rails_app
# bundle init

Gemfileが出来るので編集

\# gem "rails"

の行のコメントアウトを外す

# bundle install --path vendor/bundle
# bundle exec rails new . -B -d mysql --skip-turbolinks --skip-test

→ERROR
Could not find gem 'mysql2 (>= 0.4.4)' in any of the gem sources listed in your Gemfile.が出る

MYSQLを導入します。
https://dev.mysql.com/downloads/repo/apt/
で、パッケージをダウンロードします。
下のコマンドでインストールします。
インストール中にバージョン選択を求められます。

# sudo dpkg -i mysql-apt-config_0.8.13-1_all.deb
# sudo apt-get install mysql-server
# sudo apt-get install libmysqld-dev

MySQLを起動します。

# sudo /etc/init.d/mysql start

bundle installした後、DBを作成。

# bundle exec rake db:create
RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment
Access denied for user 'root'@'localhost'
Couldn't create 'rails_app_development' database. Please check your configuration.
rake aborted!
Mysql2::Error: Access denied for user 'root'@'localhost'
              :

こんなのがでるので

# bundle
# bundle exec rails webpacker:install

もう一度bundle exec rake db:create

warning Skipping preferred cache folder "/home/user/.cache/yarn" because it is not writable.
warning Selected the next writable cache folder in the list, will be "/tmp/.yarn-cache-1000".
Access denied for user 'root'@'localhost'
Couldn't create 'rails_app_development' database. Please check your configuration.
rake aborted!
Mysql2::Error: Access denied for user 'root'@'localhost'
                  :

権限がないので、ユーザー作って権限を与えます

sql> CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';
sql> GRANT ALL ON rails_app_development.* TO 'user'@'localhost';

config/database.ymlの設定と合わます

rails_app_testでも権限ないとでるようなので、
もう一回権限付与します。するとbundle exec rake db:create出来ました

# bin/rails s

でサーバーがちゃんと起動したので問題ないはず

http://localhost:3000/
に繋ぎます

FireShot Capture 059 - Ruby on Rails - localhost.png

おわり
いろいろ試しましたが、上の手順で良さそうです
嵌りに嵌りました

ubuntu version18.04
rbenv 2.6.3
Rails 6.0.0
10.1.41-MariaDB

その他
MySQLが起動しない

# sudo /etc/init.d/mysql start

→ERROR

下を実行

# sudo apt clean
# sudo apt install mariadb-common mariadb-server mariadb-client

→ mysql -u root する
ERROR 1698 (28000): Access denied for user 'root'@'localhost'
こんなのがでる

→ sudo mysql -u root だと入れた
Ubuntu特有の初期設定らしい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails6サーバー起動まで手順

windows10でruby on rails使えるようにします。

まずubuntuを入れます。(省略)
rbenvでRubyをインストールします。(省略)
node.jsとかBundlerを入れます。(省略)

$ mkdir rails_app
$ cd rails_app
$ bundle init

Gemfileが出来るので編集
# gem "rails"
の行のコメントアウトを外す

$ bundle install --path vendor/bundle
$ bundle exec rails new . -B -d mysql --skip-turbolinks --skip-test

→ERROR
Could not find gem 'mysql2 (>= 0.4.4)' in any of the gem sources listed in your Gemfile.が出る

MYSQLを導入します。
https://dev.mysql.com/downloads/repo/apt/
で、パッケージをダウンロードします。
下のコマンドでインストールします。
インストール中にバージョン選択を求められます。

$ sudo dpkg -i mysql-apt-config_0.8.13-1_all.deb
$ sudo apt-get install mysql-server
$ sudo apt-get install libmysqld-dev

MySQLを起動します。

$ sudo /etc/init.d/mysql start

bundle installした後、DBを作成。

$ bundle exec rake db:create
RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment
Access denied for user 'root'@'localhost'
Couldn't create 'rails_app_development' database. Please check your configuration.
rake aborted!
Mysql2::Error: Access denied for user 'root'@'localhost'
              :

こんなのがでるので

$ bundle
$ bundle exec rails webpacker:install

もう一度bundle exec rake db:create

warning Skipping preferred cache folder "/home/user/.cache/yarn" because it is not writable.
warning Selected the next writable cache folder in the list, will be "/tmp/.yarn-cache-1000".
Access denied for user 'root'@'localhost'
Couldn't create 'rails_app_development' database. Please check your configuration.
rake aborted!
Mysql2::Error: Access denied for user 'root'@'localhost'
                  :

権限がないので、ユーザー作って権限を与えます

sql> CREATE USER 'user'@'localhost' IDENTIFIED BY 'password';
sql> GRANT ALL ON rails_app_development.* TO 'user'@'localhost';

config/database.ymlの設定と合わます

rails_app_testでも権限ないとでるようなので、
もう一回権限付与します。するとbundle exec rake db:create出来ました

$ bin/rails s

でサーバーがちゃんと起動したので問題ないはず

http://localhost:3000/
に繋ぎます

FireShot Capture 059 - Ruby on Rails - localhost.png

おわり
いろいろ試しましたが、上の手順で良さそうです
嵌りに嵌りました

ubuntu version18.04
rbenv 2.6.3
Rails 6.0.0
10.1.41-MariaDB

-追記-
postgresqlだったらすんなり行けました。
はぁ・・・

その他

UbuntuでCドライブ配下に移動

$ cd /mnt/c

MySQLが起動しない

$ sudo /etc/init.d/mysql start

→ERROR

下を実行

$ sudo apt clean
$ sudo apt install mariadb-common mariadb-server mariadb-client

mysql -u root する ERROR 1698 (28000): Access denied for user 'root'@'localhost' こんなのがでる

→ sudo mysql -u root だと入れた
Ubuntu特有の初期設定らしい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのsort_byで特定の順番に並び替え

Rubyのsort_byで特定の順番に並び替え

昇順や降順に並び替えるのは、よくありますが、
プロデューサーの意向で特定の順番に並び替えたい時があります。

例えば、人気順に並び替えたい時などです。

id 順ではなく、とにかく指定した順番に並び替えたい :thinking:

例えば、こんな配列があって、

@fruites = [
  [ 1, 'apple' ],
  [ 2, 'orange' ],
  [ 3, 'banana' ],
  [ 4, 'melon' ],
  [ 5, 'peach' ],
  [ 6, 'kiwi' ],
  [ 7, 'cherry' ],
]

こんな id順で指定されたとします :arrow_down:

FRUITS_ORDER = [3, 2, 7, 5, 4, 1, 6]

sort_by に、並び替えたいid順におけるindex(位置情報)を渡してあげると、その通りに並び替えられます :relieved:

@fruites.sort_by{|fruit| FRUITS_ORDER.index(fruit[0])}
=> [[3, "banana"],
 [2, "orange"],
 [7, "cherry"],
 [5, "peach"],
 [4, "melon"],
 [1, "apple"],
 [6, "kiwi"]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

H30秋基本情報技術者試験問3システム(随時加筆

2019年3,4月の間はエンジニアの下で、Rubyを中心に色々学んだ。
その中で、H30秋基本情報技術者試験問3のコンサートに則したサイトを実際に作っていた。

5月以降も勉強しながら、少しずつ機能等を追加していたが、アウトプットしていなかった。
いつかまたRubyonRailsで似たことをするとき用に書き残しておこう。

参照

自分のサイト

コンサート問題のGithubレポジトリ
GithubPages

自分の関連アウトプット

21日目:H30秋基本情報技術者試験の問3データベース
プログラミングを2か月間、セブで学んできた

トランザクション(Paymentコントローラ)

エンジニアの下で学び、仕組みを理解し、アウトプット18日目:トランザクションって
を書いてはいたが、深くまで理解しておらず、実装時にてこずった。
modified.png
コンサートチケットの支払い時の、ポイント使用・追加あたりの、Paymentコントローラ内に実装。
※※※なお、Userテーブルに所持金カラムを追加してないので

トランザクションの流れ

  1. ユーザはポイントUser.pointを持っている。
  2. 購入時にUser.pointの一部/全部を支払額Sale.amountに充てることができる。
  3. 使用ポイントSale.used_pointが更新される
  4. 支払額から使用したUser.pointを引いたものが、決済額Payment.amountとなる。
  5. 決済額Payment.amountのうち、既定の割合が付与ポイントPayment.added_pointとなる。
  6. ユーザのポイント残高は、(支払前の)User.point - Sale.used_point + Payment.added_pointで更新される。

支払い完了の条件

  • User.point、Sale.used_point、Payment.added_pointは全て0以上(>=0)
    • モデル側のバリデーションvalidates :point, numericality: { greater_than_or_equal_to: 0 }利用
  • User.point >= Sale.used_point
    • Falseとなる操作は悪意しかないので、トランザクション外のif文で

実装自体

User.point、Sale.used_point、Payment.added_pointは全て0以上(>=0)

それぞれのモデル.rb
validates :point, numericality: { greater_than_or_equal_to: 0 } # User
validates :used_point, numericality: { greater_than_or_equal_to: 0 } # Sale
validates :added_point, numericality: { greater_than_or_equal_to: 0 } # Payment
users_controller.rb
def payment
    @user = 割愛
    @sale = 割愛
    @concert = 割愛
    @payment = Payment.new(sale_id: @sale.id, date: Date.current)
    respond_to do |format|
      if current_user.point < params_used_point 
        # 所持ポイントを超過している旨の警告文 (以下、ポイントをPと略す)
      else # 所持P範囲内で支払おうとしている場合
        begin
          ActiveRecord::Base.transaction do
            if @sale.amount <= params_used_point # 使用Pが販売額を超えてる時。
              @sale.update!(used_point: @sale.amount)
              @payment.update(amount: 0, added_point: 0) # 支払成功。決済額が0なので、追加Pもゼロ
            else
              @sale.update!(used_point: params_used_point)
              @payment.update!(amount: pay_amount, added_point: 追加P計算関数)
            end
            @user.update!(point: ユーザP更新関数)
            # 支払い完了と表示
          end
        rescue StandardError => e # トランザクション失敗したら
          logger.error e
          logger.error e.backtrace.join("\n")
          @sale = 割愛
          @concert = 割愛
          format.html { render :confirm, notice: 'エラー' }
        end
      end
    end
end

まあ、駄目な部分もあると思う。が、まあ、

Githubセキュリティアラート

キャプチャ.JPG
nokogiriに関するセキュリティアラートが来ていた。
nokogiri。。。Gemfileには書いてない。Gemfile.lockの方のみ。

確か、Gemfile.lockには、Gemfileには書いてなくても依存関係にあるものは、書かれるのだから、今回はその他gemがnokogiriに依存しているのだろう。

今回はbundle updateが適当だろう。

  • gem update
    • gem コマンドは Gemfile や Gemfile.lock とは無関係に動作
    • 被インストールgemについて,より新しいバージョンがあれば最新版をインストール
  • bundle update
    • Gemfile と Gemfile.lock に基づいて動作
terminal
bundle update nokogiri 
git add -i
git commit -m 'update nokogiri'
git push origin master'

セキュリティアラート消えた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#ruby でインタンス変数を削除・未定義状態にする ( remove_instance_variable :@some_variable )

Use #ruby to delete an instance variable and make it undefined (remove_instance_variable: @some_variable)

remove_instance_variable :@some_variable

[11] pry(main)> @a = :b
=> :b
[12] pry(main)> defined? @a
=> "instance-variable"
[13] pry(main)> remove_instance_variable :@a
=> :b
[14] pry(main)> defined? @a
=> nil

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2358

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す72(enum _scopes 編)

# はじめに

(多分)Rails 6 に追加された新機能を試す第72段。 今回は、 enum _scopes 編です。
Rails 6 では、 モデルで enum を使うときに scope を定義しないようにできるオプション :_scopes が追加されました。
Ruby 2.6.3, Rails 6.0.0.rc2 で確認しました。Rails 6.0.0.rc2 は gem install rails -v 6.0.0rc2 --prerelease でインストールできます。

(Rails6がリリースされましたが、動作確認当時は、Rails 6.0.0.rc2 が最新でした。悪しからず :bow:

$ rails --version
Rails 6.0.0.rc2

今回は、 Child モデルを作成して rails console を使って確認します。

プロジェクトを作る

rails new rails_sandbox --database postgresql
cd rails_sandbox

Child モデルを作る

name と generation の2つの属性をもつ Child モデルを作ります。
generation は enum にするため、 integer にします。

bin/rails g model Child name generation:integer

enum を定義する

Child モデルに enum を定義します。

app/models/child.rb
class Child < ApplicationRecord
  enum generation: %i[baby toddler preschool gradeschool teen young_adult]
end

seed データを作成する

seed データを作成します。

db/seeds.rb
Child.create(name: 'Andy', generation: :baby)
Child.create(name: 'Bob', generation: :toddler)
Child.create(name: 'Cindy', generation: :preschool)

マイグレーションを実行し seed データを登録する

bin/rails db:create db:migrate db:seed

rails console で確認する

rails console で確認します。

Child.baby を実行してみます。

irb(main):001:0> Child.baby
  Child Load (0.4ms)  SELECT "children".* FROM "children" WHERE "children"."generation" = $1 LIMIT $2  [["generation", 0], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Child id: 1, name: "Andy", generation: "baby", created_at: "2019-08-04 08:57:11", updated_at: "2019-08-04 08:57:11">]>

ちゃんと動作しています。
Child.not_baby を実行してみます。

irb(main):002:0> Child.not_baby
  Child Load (0.6ms)  SELECT "children".* FROM "children" WHERE "children"."generation" != $1 LIMIT $2  [["generation", 0], ["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Child id: 2, name: "Bob", generation: "toddler", created_at: "2019-08-04 08:57:11", updated_at: "2019-08-04 08:57:11">, #<Child id: 3, name: "Cindy", generation: "preschool", created_at: "2019-08-04 08:57:11", updated_at: "2019-08-04 08:57:11">]>

こちらもちゃんと動作しました。

_scopes を使う

ここからが本番です。 Child モデルを変更して _scopes を使ってみます。

app/models/child.rb
class Child < ApplicationRecord
  enum generation: %i[baby toddler preschool gradeschool teen young_adult], _scopes: false
end

rails console で確認する

修正を反映させるため reload! します。

irb(main):003:0> reload!
Reloading...
=> true

Child.baby, Child.not_baby を試すとNoMethodError が発生します。

irb(main):004:0> Child.baby
Traceback (most recent call last):
        1: from (irb):4
NoMethodError (undefined method `baby' for #<Class:0x0000555a2cfb4808>)
irb(main):005:0> Child.not_baby
Traceback (most recent call last):
        2: from (irb):5
        1: from (irb):5:in `rescue in irb_binding'
NoMethodError (undefined method `not_baby' for #<Class:0x0000555a2cfb4808>)

scope が定義されないが enum は使える

scope は定義されませんが、 それ以外の enum の機能は使えます。

Child オブジェクトの作成

irb(main):008:0> dave = Child.new(name: 'Dave', generation: :teen)
=> #<Child id: nil, name: "Dave", generation: "teen", created_at: nil, updated_at: nil>

baby? メソッド や teen? メソッド

irb(main):009:0> dave.baby?
=> false
irb(main):010:0> dave.teen?
=> true

データベースへの保存

irb(main):011:0> dave.save
   (0.2ms)  BEGIN
  Child Create (0.4ms)  INSERT INTO "children" ("name", "generation", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "Dave"], ["generation", 4], ["created_at", "2019-08-04 09:15:05.113455"], ["updated_at", "2019-08-04 09:15:05.113455"]]
   (7.1ms)  COMMIT
=> true

データベースから検索して、 dave の generation を確認

irb(main):012:0> dave = Child.last
  Child Load (0.6ms)  SELECT "children".* FROM "children" ORDER BY "children"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<Child id: 4, name: "Dave", generation: "teen", created_at: "2019-08-04 09:15:05", updated_at: "2019-08-04 09:15:05">
irb(main):013:0> dave.generation
=> "teen"

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try072_enum_disable_scopes

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecord == GORM

Railsだと

product = Product.find(id)

GORMだと

product := models.Product{ID: id}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails console 上で実行時間を計測する

Rails Tutorial 5日目。
現在第8章を勉強中。

知りたいこと

find_by と find_by_x ってどっちが速いの?

6.1.4 ユーザーオブジェクトを検索する 演習1

nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。

find_by が古いタイプと言いながら、このあとの演習でも find_by がたくさん出てくるから、やっぱり古くないのではないか???(互換性があるから update されてないだけ?)

追記:@scivola さんがコメントで勘違いを指摘してくださいました。ありがとうございます!
古いタイプの find_by == find_by_x ということだったのか~!

やりたいこと

find_by と find_by_x ってどっちが速いのか実行時間を計測してみたい。

ほかに知らないこと

  • 実行時間のはかり方 参考
  • rails console 上の SQL 出力の消し方(じゃま…) 参考

関数定義

rails_console
def calc_find_by(count:, id:)
  # SQL の出力を抑える
  old_logger = ActiveRecord::Base.logger
  ActiveRecord::Base.logger = nil
  # Benchmark モジュールを使う
  Benchmark.bm 10 do |r|
    # find_by(id: id) を計測
    # 結果出力時のタイトル設定
    r.report "find_by" do
      count.times do
        User.find_by(id: id)
      end
    end

    # find_by_id(id) を計測
    r.report "find_by_id" do
      count.times do
        User.find_by_id(id)
      end
    end
  end
  # SQL 出力抑制を元に戻す
  ActiveRecord::Base.logger = old_logger
end

実行結果

id : 3 は nil。

10000回
>> calc_find_by count:10000, id: 3
                 user     system      total        real
find_by      0.956429   0.092523   1.048952 (  1.050382)
find_by_id   0.957591   0.100028   1.057619 (  1.059075)
100000回
>> calc_find_by count:100000, id: 3
                 user     system      total        real
find_by      9.854176   0.629422  10.483598 ( 10.498670)
find_by_id   9.824362   0.667503  10.491865 ( 10.506455)

id : 2 は nil ではない。

10000回
>> calc_find_by count:10000, id: 2
                 user     system      total        real
find_by      1.278680   0.063538   1.342218 (  1.344218)
find_by_id   1.267526   0.059938   1.327464 (  1.328699)
100000回
>> calc_find_by count:100000, id: 2
                 user     system      total        real
find_by     12.492160   0.747277  13.239437 ( 13.251859)
find_by_id  12.435650   0.863175  13.298825 ( 13.311519)

そこまで大きく差が出るほど早さに影響しているわけではなさそう。
↓の通り、SQL 文は一緒だもんね。

>> User.find_by(id: 3)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
=> nil
>> User.find_by_id(3)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
=> nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

イナズマ ワーク4日目

correct_num = rand(10)
trial_number = 1

while true
  puts "0~9の数字を当ててください"
  input = gets.to_i

  if correct_num > input
    puts "もっと大きな数です"
    trial_number += 1
  elsif correct_num < input
    puts "もっと小さな数です"
    trial_number += 1
  else
    puts "正解です"
    puts "#{trial_number}回で当たりました!"
    break
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インクリメンタルサーチ+複数語検索を作る

Ruby 2.5.1
Rails 5.2.3

今回作るもの

インクリメンタルサーチ + 複数語検索
スペースがある場合複数のキーワードで検索することができ、後の方の検索語でヒットするものから上部に表示します。
画面がチカチカしないように、ゆっくり表示します。

画像は現在作っている、色の図鑑みたいなアプロケーションです。

検索が好きな方、インクリメンタルサーチをつけたい方のお役に立てば幸いです。

Image from Gyazo

Gyazo.gifの無料枠で3つ検索するの撮り直しにコード書くよりも時間がかかっています。

ビューの準備

入力フォームと、検索結果を表示するための箱を用意してください。

フォームに入力された値を送って、帰ってきた結果を表示

$(function(){
    $(".入力欄のクラス").on("keyup",function(){
      var input = $(".入力欄のクラス名").val();
      $(".検索結果表示部分のクラス").empty();
//非同期通信で、フォームのキーワードを送ります。
      $.ajax({
        type: 'GET',
        url: '/search/new',
        data: { keyword: input },
        dataType: 'json',
      })
      .done(function(colors){
//検索結果をcolorsという名前の配列として受け取ります。もし、検索結果がある場合は以下の処理を行います。
        if (colors.length !== 0) {
          $('.検索結果表示部分のクラス').empty();
//検索結果として帰ってきた配列それぞれに対して以下の処理を行います。
          colors.forEach(function(color){
//検索結果として表示させたいHTMLを記述してください。
            var html = `<div class="result">
                          <div class="result__text">
                            ${color.name}
                            <br>
                            (${color.red},${color.green},${color.blue})
                          </div>
                          <a class="result__cell" style="background-color:rgb(${color.red},${color.green},${color.blue});" href=""></a>
                        </div>`
//HTMLを検索結果表示部分に差し込みます。(画面がチカチカするの防ぐため、200ミリ秒でフェードインしながら)
            $('.検索結果表示部分のクラス').append(html).hide().fadeIn(200);            ;
          });
        }
        else {
//検索した結果、0件だった場合
          $(".検索結果表示部分のクラス").empty();
//検索結果がないことを示すHTMLを記述します。
          var html = `<div class="result">
                          <div class="result__text">
                            お探しの色は見つかりませんでした
                          </div>
                          <a class="result__cell" href="/color/new"></a>
                        </div>`
            $('.検索結果表示部分のクラス').append(html);
        }
      })
    });
});

コントローラ

def new
#フォームから送られてきたキーワードをスペースで区切って配列に
    keywords = params[:keyword]&.split(/[[:blank:]]+/)
#検索結果を入れるための空の配列を用意
    @colors = []
#複数に分割したキーワードで検索
    keywords.each do |keyword| 
      next if keyword == "" #キーワードが空であれば、すぐ次の検索をかける
#nameのカラムから探す
      @colors += Color.where('name LIKE(?)', "%#{keyword}%") 
    end 
#重複するものがあれば消去
    @colors.uniq! 
#後の検索語でヒットしたものから、表示配列の順番をひっくり返す
    @colors.reverse!
    respond_to do |format|
      format.html
      format.json
    end
  end

インクリメンタルサーチは、せっかちさんのための検索です。きっと複数語検索をかけているということは、1語目の検索結果に満足していないということです。ですので、リバースをかけることにしました。

*別途、jbuilderは用意する必要があります。

!について

!をつけると破壊的なメソッドになるという説明がありますが、どういうことでしょうか。簡単な例で実験してみましょう。
下記の例でjoinは配列の出力時の改行を消すために付記したものです。

#!なしの場合
numbers = [1,2,3,4,5]
puts numbers.reverse.join #=> 54321
puts numbers.join #=> 12345

#!の場合
numbers = [1,2,3,4,5]
puts numbers.reverse!.join #=> 54321
puts numbers.join #=> 54321

今回の場合ですと、!がついたreverseの場合はレシーバ(.の左側)自体に変更が及んでいます。イメージとしては下記のような形です。

numbers = [1,2,3,4,5]
numbers = numbers.reverse
puts numbers.join #=> 54321

!の有無でどのような違いになるのか、勘違いしてしまうと大変なので、使用する場合にはそれぞれのメソッドのリファレンスをご覧になることをおすすめします。

終わりに

検索フォーム適当に作って、ページ遷移させて適当に値引っ張ってくるだけで終わることもできるのですが、色々と工夫ができます。こういうの考えている時ってサーバーサイド楽しいな〜と感じます。是非是非、色々遊んでみてください!
ありがとうございました!

SQLの軽量化や正規表現について

非常に有意義なコメントをいただいたので是非お読みになってください!
せっかくなので本文はそのまま改善の余地のある見本の形で残しておくことにします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js + GraphQL + Ruby on Railsで作ったアプリにJWT認証を追加する方法

本記事ではフロントエンドに Nuxt.js(Vue.js)、バックエンドに Ruby on Rails 、APIに GraphQL を採用したアプリケーションに、JWTトークンによる認証 を追加する方法についてまとめます。

login.gif

題材

サンプルとして以下チュートリアルで作成したToDoリストを使用します。

Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(前編)
Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(後編)

JWTでの認証フロー

今回実装するJWTでの認証フローを図にまとめました。

Untitled_graphql-jwt_-_Cacoo.png

トークンの発行・保存・付与・検証がめんどくさそうに見えるかもしれませんが、フロントエンド側はAuth Moduleが、バックエンド側はknockがトークンをいい感じに処理してくれるので、安心してください。

実装(バックエンド)

ユーザモデルを定義する

認証単位となるモデル(User)を生成します。

$ bundle exec rails g model user email:string password_digest:string
$ bundle exec rails db:migrate

Userはパスワードをハッシュ化して管理するので、モデルに has_secure_password を宣言します。

app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

また、Gemfileの bcrypt のコメントを外します。

Gemfile
# ・・・中略・・・

# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7

# ・・・中略・・・

テスト用のUserを seed.rb に追記します。

db/seeds.rb
# ・・・中略・・・

User.create(
  email: 'test@example.com',
  password: 'xxxxxxxx',
  password_confirmation: 'xxxxxxxx'
)
$ bundle exec rails db:seed

knockをインストールする

Gemfileにknockを追加し、インストールします。

Gemfile
gem 'rack-cors'
gem 'graphql'
gem 'knock' # ★追加
$ bundle install

Rails6だとknockのautoloadに失敗するのでinitializerで明示的にrequireします。

config/initializers/eager_load_knock.rb
require 'knock/version'
require 'knock/authenticable'

ジェネレータを実行します。

$ rails generate knock:install

ログイン/ログアウトのエンドポイントを準備

ジェネレータでControllerを生成します。

$ rails generate knock:token_controller user

各Controllerにて認証処理を行えるようにするため、 ApplicationController にてmoduleをincludeします。

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Knock::Authenticable
end

GraphqlController のbefore_actionとして認証処理を追加します。

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  before_action :authenticate_user

  # ・・・中略・・・
end

今回、セッションは使わないのとCORS設定済みであることを考慮して、CSRF対策を解除します。

config/application.rb
# ・・・中略・・・

module RailsNuxtGrapshqlTodoapp
  class Application < Rails::Application
    config.load_defaults 6.0
    config.api_only = true
    config.action_controller.default_protect_from_forgery = false 
  end
end

動作確認(バックエンド)

サーバ起動してInsomniaを使ってリクエストを送信してみます。

$ bundle exec rails s

未認証の状態でGraphQL Queryを送信しても、401 が返ってきます。

Insomnia__onescene__–_tasks.png

/user_token へリクエストすると、JWTトークンが返ってきます。

Insomnia__onescene__–_user_token.png

Bearerの設定でTOKEN欄に上記JWTトークンを記載します。

Insomnia__onescene__–_tasks.png

この状態で再度GraphQL Queryを送信すると、 200 が返ってきました。
JWT認証が機能していますね。

Insomnia__onescene__–_tasks.png

実装(フロントエンド)

Auth Moduleをインストールする

npmでインストールします。
Auth Moduleは store/index.js が存在していないとエラーを出すので、空のファイルを作成しておきます。

$ npm install @nuxtjs/auth @nuxtjs/axios
$ touch store/index.js

nuxt.config.jsmodules, axios, auth, apollo を追記します。

  // ・・・中略・・・
  modules: [
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo',
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],
  axios: {
    baseURL: 'http://localhost:3000/'
  },
  auth: {
    strategies: {
      local: {
        endpoints: {
          login: { url: 'user_token', method: 'post', propertyName: 'jwt' },
          user: false,
          logout: false
        }
      }
    }
  },
  // ・・・中略・・・
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:3000/graphql',
        getAuth: () => ''
      }
    }
  }
  // ・・・中略・・・

ログイン画面を準備

rails_nuxt_grapshql_todoapp_front_-_rails_nuxt_grapshql_todoapp_front.png

/login に相当する画面およびログイン処理を実装します。

pages/login.vue
<template>
  <v-container>
    <v-row>
      <v-col cols="6" offset="3">
        <v-card>
          <v-card-title>Login</v-card-title>
          <v-card-text>
            <v-form>
              <v-container>
                <v-row>
                  <v-col cols="6">
                    <v-text-field v-model="email" label="Email" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col cols="6">
                    <v-text-field v-model="password" type="password" label="Password" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col>
                    <v-btn @click="login()">
                      Login
                    </v-btn>
                  </v-col>
                </v-row>
              </v-container>
            </v-form>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  middleware({ store, redirect }) {
    window.console.log(store.state.auth.loggedIn)
    if (store.state.auth.loggedIn) {
      return redirect('/')
    }
  },
  data() {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    async login() {
      try {
        await this.$auth.loginWith('local', {
          data: {
            auth: {
              email: this.email,
              password: this.password
            }
          }
        })
        await this.$apolloHelpers.onLogin(this.$auth.getToken('local').match(/^Bearer[ ]+([^ ]+)[ ]*$/i)[1])
        this.$router.push('/')
      } catch (e) {
        window.console.log(e)
      }
    }
  }
}
</script>

無名middlewareを用いることで、既にlogin済みの状態でこのページを開くと、'/' でリダイレクトするようにしています。

methodsの login() がLOGINボタンを押したときの処理です。
Auth Moduleでログインをした後で、Apollo ModuleへJWTトークンをセットしています。

ログアウトボタンを追加

rails_nuxt_grapshql_todoapp_front_-_rails_nuxt_grapshql_todoapp_front.png

ログイン中の場合のみ、メニューバーにログアウトボタンを表示します。

layouts/default.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title v-text="title" />
      <div class="flex-grow-1" />
      <span v-if="loggedIn" @click="logout()">Logout</span>
    </v-app-bar>
    <v-content>
      <nuxt />
    </v-content>
    <v-footer center>
      <v-layout justify-center>
        <span>&copy; 2019 Yuhei Okazaki. All Rights Reserved.</span>
      </v-layout>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      title: 'Tasks'
    }
  },
  computed: {
    loggedIn() {
      return this.$auth.loggedIn
    }
  },
  methods: {
    async logout() {
      try {
        await this.$auth.logout()
        await this.$apolloHelpers.onLogout()
        this.$router.push('/login')
      } catch (e) {
        window.console.log(e)
      }
    }
  }
}
</script>

methodsの logout() がLOGOUTを押したときの処理です。
Auth Moduleでログアウトをした後で、Apollo Moduleもログアウトしています。

未認証時のリダイレクトを追加

このままだと、ログインしていない状態でもタスク一覧画面を開けてしまうので、ログインしていないときには /login へ飛ばすようmiddlewareを設定します。

pages/index.vue
// 中略
export default {
  middleware: 'auth'
  // 中略
}

動作確認(全体)

冒頭の画像のように、ログインしたときのみタスク一覧が表示されます。

login.gif

まとめ

本記事ではバックエンドにRuby on Rails、フロントエンドにNuxt.js、APIにGraphQLを採用したアプリケーションにJWT認証を追加しました。
新規登録画面やタスクとユーザの紐付け等、未実装の処理は多数あるものの、認証というアプリケーションを実装するときの最初の壁は越えられたかと思います。

knockやAuth Module、Apollo Moduleを用いたことで、トークン操作を意識せず簡単に認証追加できたので、ぜひお試しください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails チュートリアル:第1章

この章でやったこと、学んだこと

  • Ruby on Railsチュートリアルとは何か
  • 開発の準備(環境構築やRailsのインストール等)
  • Hello_appの作成
  • Gitによるバージョン管理

この章の内容の中で、覚えておきたいこと

  • $ git checkout -b 新しいブランチ名で簡単にブランチを作成してmasterブランチから切り替えることができる。
  • $ git branchでブランチの一覧と現在のブランチを確認できる。
  • 作成したブランチで作業が完了したら、$ git commit -a -m "コミットするメッセージ"を実行。
  • ファイルの変更が終わったら、$ git checkout masterでmasterブランチに移動し、$ git merge ブランチ名でmasterブランチにこの変更をマージ (merge) する。
  • 必須ではないが、使い終わったブランチを削除しておきたい場合は、$ git branch -d 削除したいブランチ名を実行する。

この章を終えて感じたこと

Gitはある程度理解したつもりでいたが、Branchの使い方の理解が足りなかった。Git以外はほぼ理解できていたので、スラスラと進めることができた。第2章からも頑張りたいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む