- 投稿日:2019-08-29T23:06:36+09:00
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 } endMigration
$ 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"]}以上。
- 投稿日:2019-08-29T22:54:04+09:00
【RubyonRails入門】before_action
1. before_cation とは
before_cation
とは、何かのアクションをする前に実行することができるメソッド。例えば、ユーザーがログインしているかどうかを、各ページに移動するごとに確認するときに使用する。2. before_cationの簡単な例
・各ページのコントローラーが実行される前に、コントローラ全体を司っているコントローラの
before_action :get_category
が呼ばれる。/app/controllers/application_controller.rbclass ApplicationController < ActionController::Base # 各ページのコントローラーが実行される前に、 # ↓↓↓(get_test)が呼ばれる。 before_action :get_test def get_test @tests = Test.all end end
- 投稿日:2019-08-29T21:00:13+09:00
オススメ! Evryday Rails - RSpecによるRailsテスト入門を購入しました!
タイトルの通りEvryday Rails - RSpecによるRailsテスト入門を購入しました。
購入しようと思った理由は、RSpecを書いてみたくて、いままで自分で調べながらやっていたけど、情報がバラバラで自分ではいまいち理解できずモヤモヤしていたから。今日は3章までやってみたが、とにかく分かりやすい!モヤモヤしてたのがス~と消えてく感じです。これから頑張っていきます!
余談
Windows 10 Home
購入してさっそく勉強を始めようとして、Google Chromeをダブルクリックしたらなぜか開かない
昨日まで正常に動いたのに。Microsoft Edgeを開いたらこれは正常に起動した。調べていろいろ試したけど変化が無かったので、結局Chromeを削除し、再インストールしたら起動するようになりました。なぜ急に壊れた???
- 投稿日:2019-08-29T19:56:03+09:00
Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #13 パスワード再設定編
こんな人におすすめ
- プログラミング初心者でポートフォリオの作り方が分からない
- Rails Tutorialをやってみたが理解することが難しい
前回:#12 ActionMailer, アクティベーション編
次回:準備中こんなことが分かる
- パスワードを再設定させる方法
- トークンとダイジェストを生成、認証する方法
- メーラーの使い方
- RSpecでコントローラのインスタンス変数を用いる方法
今回の流れ
- パスワード再設定のイメージをつかむ
- ビューを作る
- トークンとダイジェストを生成しURL入りメールを送信する
- URLの情報が正しいか確認し再設定する
パスワード再設定のイメージ
パスワード再設定は#12で紹介したアクティベーションと似ています。
ぜひ比較しながらご覧ください。
アクティベーションの時はビューが必要ありませんでした。
しかし今回は2つのビューと4つのアクションが必要です。今回も先にコントローラとリソースを生成します。
bash$ rails g controller PasswordResets new create edit update
config/routes.rb# 中略 resources :password_resets, only: [:new, :create, :edit, :update]パスワード再設定までをつくる手順
以下はパスワード再設定をつくるときの手順です。
- パスワード再設定用のビューを作る(new)
- トークンとダイジェストを生成し、メールを送信する(create)
- メール内のURLにトークンとメールアドレスを忍ばせる
- URLをクリックしたらURL内の情報が有効か確認する(edit)
- 確認した情報や入力したパスワードが正しければ再設定が完了する(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>パスワード再設定画面(メール入力)を作る
続いてメールを送るまでのパスワード再設定画面を作ります。
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>パスワード再設定画面(パスワード入力)を作る
ここはちょっとクセがあります。
なぜなら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>トークンとダイジェストを生成しメールを送信する(create)
続いてはトークンとダイジェストの生成からメール送信までを記述しましょう。
手順は以下の通りです。
- ダイジェスト用の属性を与える
- トークンとダイジェストを生成、認証するメソッドを確認する
- 再設定用トークンとダイジェストを生成、メールを送信するメソッドをつくる
ダイジェスト用の属性を与える
その前にマイグレーションで属性を与えます。
以前の記事と異なるのは、再設定の有効期限を記す属性も加えるという点です。bash$ rails g migration add_reset_to_users reset_digest:string reset_sent_at:datetime $ rails db:migrateトークンとダイジェストを生成、認証するメソッドを確認する
トークンやダイジェストの生成、認証するメソッドは#9ですでに紹介しています。
すでに#9をお読みの方は飛ばしてください。
それ以外の方は以下のメソッドを追加してください。app/models/user.rbclass 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.rbclass 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.rbclass 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.rbclass 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.rbclass 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情報が正しいか確認し再設定する(edit, update)
URLがクリックされたらeditアクションを呼び出します。
ここで行うことは3つあります。
- Userモデルを特定する
- 存在するか、トークンは正しいのかを確かめる
- URLが期限切れでないかを確かめる
この3つに関してはupdateアクションでも同じことを行います。
ということはこうした方がスッキリします。
- 上記3つをメソッド化する
- editとupdate時に呼び出す
アクションの直前にメソッドを呼び出すにはbefore_actionを使います。
一方updateアクションはフォームにパスワードが入力された時の処理を書きます。
今回は3つのケースに対応します。
- 入力されていない時
- 無効なパスワードの時
- 正しい時
こちらはupdateアクションのみの振る舞いなので、直接書き込みます。
では、これらを踏まえて実装しましょう。app/controllers/password_resets_controller.rbclass 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 end1つ言い忘れたことがあります。
それはcheck_expiration内にあるpassword_reset_expired?メソッドです。期限切れかどうかを判別する処理に関しては、別途Userモデルにメソッドを用意した上で実装しています。
そちらのメソッドを紹介します。
app/models/user.rbclass User < ApplicationRecord # 中略 def password_reset_expired? reset_sent_at < 2.hours.ago end private # 中略これでパスワード再設定に関する全ての実装が完了しました。
テストを書く
最後にパスワード再設定に関するテストを完成させます。
いくつかあるので順に見ていきます。メーラーテストを書く
まずはメーラーのテストです。
メールの内容についてテストします。
なおメール本文に関してはデコードを行って検証しています。spec/mailers/user_mailer_spec.rbrequire "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.rbrequire '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ではなく、コントローラ内インスタンス変数のuserをuserとして取得し直しています。なぜでしょう。
理由は以下の通りです。
- edit_password_reset_pathの引数に当たるreset_tokenは、attr_accessorによって生成された仮属性です。
- 仮属性なのでletで生成したFactoryBotのuserにはreset_tokenが存在しません。よってエラーになります。
- したがってreset_tokenが代入されたPasswordResetsコントローラのインスタンス変数userを使用する必要があります。
というわけで、コントローラのインスタンス変数のuserが必要です。
そこでassignsを使用します。
assignsはコントローラのインスタンス変数を取得します。そのためにはgem 'rails-controller-testing'が必要です。
(とてもためらいましたが)導入しましょう。Gemfilegroup :development, :test do + gem 'rails-controller-testing' endbash$ bundle installこれで問題なく動作します。
テストを走らせてみましょう。bash$ rails spec
問題なければ、以上でテストは終了です。
追記:ですます調に統一しました
今回から語尾を「ですます」に統一しました。
情報が統一されて見やすいかなと。
分かりやすい記事になるよう努めます。やさしい記事の書き方↓
これであなたのQiita記事もランキング入り!?@jnchitoによる編集リクエスト解説(解説動画付き)
- 投稿日:2019-08-29T17:51:26+09:00
【Rails】個人的に思いついた二重サブミット対策【意見も募集】
二重サブミットをどうにか防止したかった。
具体的には下記のようなケースで防止したかったです。
- ブラウザバックか戻るボタンで戻って、再度保存
- 完了ページでブラウザリロードcontorllerの処理
inquiries_controller.rbdef new @inquiry = Inquiry.new end def confirm @inquiry = Inquiry.new(inquiry_params) render :new unless @inquiry.valid? end def done @inquiry = Inquiry.new(inquiry_params) if params[:back].present? render :new return end if @inquiry.save #メールを送付 InquiryMailer.send_inquiry_to_user(@inquiry).deliver else render :new end end入力画面▶️確認画面▶️完了画面の順に移り変わります。
確認画面と完了画面に関してはPostで表示させています。
完了画面にリダイレクトさせるPRGパターンもあるようですが、今回は採用していません。個人的に思いついた二重サブミット対策
完了画面のPost処理の最後にセッションのauthenticity_tokenのパラメータを削除する。
session[:_csrf_token] = nilRailsはデフォルトでCSRF対策をしてくれます。
class ApplicationController < ActionController::Base protect_from_forgery with: :exception end上記によってファームを生成する際にauthenticity_tokenがフォームとセッションに発行され、その二つが合っているかを判断します。
よってPostの最後にセッション側のトークンを削除することで、以降フォームとセッションのトークン情報が合致しなくなるためリロードやブラウザバックをしてもInvalidAuthenticityTokenとしてエラーになります。これで二重サブミットの対策になるんじゃないかと。(InvalidAuthenticityTokenの場合のエラーページを表示させるようにすればユーザーも戸惑わないはず)
セッションのトークンを削除することによるCSRF対策の影響も恐らくないのではと。
最後に
まだまだRailsに関しては初心者で、他の方法が思いつかなかったので他の人は普段どうやって実装しているのか知りたいです!!!!!!
ぜひ教えてください!!!!!!!!!!!!
- 投稿日:2019-08-29T16:49:39+09:00
【Qiitaクローン】1.プロジェクトの開始
はじめに
未経験からWebエンジニア(サーバサイド)へ転職するため勉強中です。
Qiitaのクローンを一から作成しながら、こちらに投稿することでより理解を深めていけたらなと思います。※ cloud9にて開発していきます。
Rails 5.2.3
Ruby 2.5.3 [x86_64-linux]機能一覧
- ユーザ登録/ログイン認証機能
- 記事の投稿/一覧機能
- 記事へのコメント機能
- フォロー機能
- いいね機能
- 検索機能
- 通知機能
今の所これくらいで必要があれば随時追加していきます。
プロジェクト開始
$ rails new qiita_clone --database=mysql$ rails new でRailsプロジェクトを立ち上げます。データベースはMySQLを使います。
$ rails db:createqiita_clone_development データベースを作成します。
$ rails sサーバを起動し、“Yay! You’re on Rails!” の表示を確認。
初日なのでここまで!
- 投稿日:2019-08-29T16:42:30+09:00
【Rails】rails webpacker:install に失敗する場合の対処法
概要
Rails 5.1 + Vue.js で開発を行う - part1 環境構築を参考に構築済みのrails環境にvue.jsを追加しようとしていた。
webpackerをインストールするため、$rails webpacker:install
を実行したらコケる現象に遭遇したのでその解消法を記載する。環境
- ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]
- Rails 5.2.3
- yarn 1.17.3
rails webpacker:install した時次のエラーが出力される
Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/hoge/projects/testapp/config/webpacker.yml
どうやらtestapp/config配下にwebpacker.ymlがないことが原因らしい。
webpacker.ymlを生成するために$ rails webpacker:install
してるのに....apple-no-MBP:testapp hoge$ rails webpacker:install /Users/hoge/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/twitter-4.8.1/lib/twitter/cursor.rb:51: warning: circular argument reference - collection Faraday::Builder is now Faraday::RackBuilder. rails aborted! Don't know how to build task 'webpacker:install ' (See the list of available tasks with `rails --tasks`) Did you mean? webpacker:install webpacker:install:vue webpacker:install:erb webpacker:install:elm webpacker:binstubs /Users/hoge/projects/testapp/bin/rails:9:in `<top (required)>' /Users/hoge/projects/testapp/bin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' (See full trace by running task with --trace) apple-no-MBP:testapp hoge$ bundle exec rails webpacker:install /Users/hoge/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/twitter-4.8.1/lib/twitter/cursor.rb:51: warning: circular argument reference - collection Faraday::Builder is now Faraday::RackBuilder. /Users/hoge/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/twitter-4.8.1/lib/twitter/cursor.rb:51: warning: circular argument reference - collection Faraday::Builder is now Faraday::RackBuilder. RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment rails aborted! Webpacker configuration file not found /Users/hoge/projects/testapp/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/hoge/projects/testapp/config/webpacker.yml /Users/hoge/projects/testapp/config/environment.rb:5:in `<top (required)>' /Users/hoge/projects/testapp/bin/rails:9:in `<top (required)>' /Users/hoge/projects/testapp/bin/spring:15:in `require' /Users/hoge/projects/testapp/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Caused by: Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/hoge/projects/testapp/config/webpacker.yml /Users/hoge/projects/testapp/config/environment.rb:5:in `<top (required)>' /Users/hoge/projects/testapp/bin/rails:9:in `<top (required)>' /Users/hoge/projects/testapp/bin/spring:15:in `require' /Users/hoge/projects/testapp/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Tasks: TOP => app:template => environment (See full trace by running task with --trace) apple-no-MBP:testapp hoge$ apple-no-MBP:testapp hoge$ apple-no-MBP:testapp hoge$ bundle exec rails webpacker:install /Users/hoge/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/twitter-4.8.1/lib/twitter/cursor.rb:51: warning: circular argument reference - collection Faraday::Builder is now Faraday::RackBuilder. /Users/hoge/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/twitter-4.8.1/lib/twitter/cursor.rb:51: warning: circular argument reference - collection Faraday::Builder is now Faraday::RackBuilder. RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment rails aborted! Webpacker configuration file not found /Users/hoge/projects/testapp/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/hoge/projects/testapp/config/webpacker.yml /Users/hoge/projects/testapp/config/environment.rb:5:in `<top (required)>' /Users/hoge/projects/testapp/bin/rails:9:in `<top (required)>' /Users/hoge/projects/testapp/bin/spring:15:in `require' /Users/hoge/projects/testapp/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Caused by:Errno::ENOENT: No such file or directory @ rb_sysopen - /Users/hoge/projects/testapp/config/webpacker.yml /Users/hoge/projects/testapp/config/environment.rb:5:in `<top (required)>' /Users/hoge/projects/testapp/bin/rails:9:in `<top (required)>' /Users/hoge/projects/testapp/bin/spring:15:in `require' /Users/hoge/projects/testapp/bin/spring:15:in `<top (required)>' ./bin/rails:3:in `load' ./bin/rails:3:in `<main>' Tasks: TOP => app:template => environment (See full trace by running task with --trace)エラー遭遇までに実行したことなど
rails環境にvue.jsを導入する時はたぶん以下の手順が一般的かも
- Gemfileに
gem 'webpacker', github: 'rails/webpacker'
を書き込んで$bundle install
→OK- yarnのインストール。
$ brew install yarn
or$brew upgrade yarn
→OK- webpackerのインストール。
$ rails webpacker:install
→エラー発生対処法
$ rails webpacker:install
する前にwebpacker.ymlをつくった。めちゃくちゃ野蛮な解決法。
一応同様のissueはすでに上がっており、そこに載ってた対処法。
https://github.com/rails/webpacker/issues/940以下のソースコードをコピペ→config配下にwebpacker.ymlをつくってその中にペースト。
https://raw.githubusercontent.com/rails/webpacker/master/lib/install/config/webpacker.ymlおしまい。たぶんyarnのバージョンが原因な気はしている。
- 投稿日:2019-08-29T16:20:40+09:00
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 endchangeメソッドがサポートしているメソッドは以下。
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_tablesqlが絡んだりする複雑なマイグレーションを書くときは 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
- 投稿日:2019-08-29T15:38:25+09:00
rails6/windows10/ubuntu/mySQL
windows10でruby on rails使えるようにします。
まずubuntuを入れます。(省略)
rbenvでRubyをインストールします。(省略)
node.jsとかBundlerを入れます。(省略)# mkdir rails_app # cd rails_app # bundle initGemfileが出来るので編集
\# 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-devMySQLを起動します。
# sudo /etc/init.d/mysql startbundle installした後、DBを作成。
# bundle exec rake db:createRAILS_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/
に繋ぎますおわり
いろいろ試しましたが、上の手順で良さそうです
嵌りに嵌りました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特有の初期設定らしい
- 投稿日:2019-08-29T15:38:25+09:00
rails6サーバー起動まで手順
windows10でruby on rails使えるようにします。
まずubuntuを入れます。(省略)
rbenvでRubyをインストールします。(省略)
node.jsとかBundlerを入れます。(省略)$ mkdir rails_app $ cd rails_app $ bundle initGemfileが出来るので編集
# 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-devMySQLを起動します。
$ sudo /etc/init.d/mysql startbundle installした後、DBを作成。
$ bundle exec rake db:createRAILS_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/
に繋ぎますおわり
いろいろ試しましたが、上の手順で良さそうです
嵌りに嵌りましたubuntu version18.04
rbenv 2.6.3
Rails 6.0.0
10.1.41-MariaDB-追記-
postgresqlだったらすんなり行けました。
はぁ・・・その他
UbuntuでCドライブ配下に移動
$ cd /mnt/cMySQLが起動しない
$ sudo /etc/init.d/mysql start→ERROR
下を実行
$ sudo apt clean $ sudo apt install mariadb-common mariadb-server mariadb-clientmysql -u root する ERROR 1698 (28000): Access denied for user 'root'@'localhost' こんなのがでる
→ sudo mysql -u root だと入れた
Ubuntu特有の初期設定らしい
- 投稿日:2019-08-29T15:27:51+09:00
ECS FargateでRails動かそうとして2ヶ月かかって手元に残ったもの【3.Fargate編】
弊社の基幹システムをVPSからAWSに移行するにあたって、まずはrailsを動かしてみる、という段階で無事に死にました☆
今後のための備忘録です。前記事:ECS FargateでRails動かそうとして2ヶ月かかって手元に残ったもの【2.Build編】
前回まででやったこと
今回やったこと
今回はいよいよコンテナをfargteで動かして、ALBも用いて公開してみようと思います。
VPC
RDS
- エンジンのタイプはmysqlで設定して、バージョンも合わせます。
- データベースの承認情報は
docker-compose
ファイルなどで設定した環境変数と合わせます。- VPCはデフォルトのもの、セキュリティグループはデフォルトを消して新規作成します。
ALB
「サービス」 → 「ES2」 → 「ロードバランサー」を選ぶ。
IAM
ECSのアクセス権限を設定します。
「サービス」 → 「IAM」 → 「ロール」 → 検索ボックスに「ecs」と入力して、「ecsTaskExecutionRole」を選択ECS
「サービス」 → 「ECS」
タスク定義
「コンテナの追加」がポイントになります。
ECRで設定した「web」と「server」のリポジトリをそれぞれ指定します。クラスター
サービス
VPCを設定します。(点線部上)
次にロードバランサーも設定します。
これでサービスを動かせば、ひとまず設定は完了です。
起動確認
「サービス」 → 「EC2」 → 「ロードバランサー」 を選択して下部のメニューにアドレスがあるのでそこからコピペしましょう。
最後に
私自身、手探りで2ヶ月かかって、やっと起動にこぎつけました。
まだまだ設定が十分でない部分はあると思います。
今後も自分自身の備忘録として、更新を続けていきたいと思います。ここまでできたこと
次記事:S3編。。。?
- 投稿日:2019-08-29T15:21:34+09:00
どうしてもRails の flash[:notice]が効かない!
formタグを直書きとかしてませんか?
ちゃんと
form_tag
を使ってauthenticity_token
が設定されてるといけますお。<%= form_tag("/save") do %> <% end %>
- 投稿日:2019-08-29T13:41:11+09:00
H30秋基本情報技術者試験問3システム(随時加筆
2019年3,4月の間はエンジニアの下で、Rubyを中心に色々学んだ。
その中で、H30秋基本情報技術者試験問3のコンサートに則したサイトを実際に作っていた。5月以降も勉強しながら、少しずつ機能等を追加していたが、アウトプットしていなかった。
いつかまたRubyonRailsで似たことをするとき用に書き残しておこう。参照
自分のサイト
コンサート問題のGithubレポジトリ
GithubPages自分の関連アウトプット
21日目:H30秋基本情報技術者試験の問3データベース
プログラミングを2か月間、セブで学んできたトランザクション(Paymentコントローラ)
エンジニアの下で学び、仕組みを理解し、アウトプット18日目:トランザクションって
を書いてはいたが、深くまで理解しておらず、実装時にてこずった。
コンサートチケットの支払い時の、ポイント使用・追加あたりの、Paymentコントローラ内に実装。
※※※なお、Userテーブルに所持金カラムを追加してないのでトランザクションの流れ
- ユーザはポイントUser.pointを持っている。
- 購入時にUser.pointの一部/全部を支払額Sale.amountに充てることができる。
- 使用ポイントSale.used_pointが更新される
- 支払額から使用したUser.pointを引いたものが、決済額Payment.amountとなる。
- 決済額Payment.amountのうち、既定の割合が付与ポイントPayment.added_pointとなる。
- ユーザのポイント残高は、(支払前の)
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)
それぞれのモデル.rbvalidates :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 } # Paymentusers_controller.rbdef 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セキュリティアラート
nokogiriに関するセキュリティアラートが来ていた。
nokogiri。。。Gemfileには書いてない。Gemfile.lockの方のみ。確か、Gemfile.lockには、Gemfileには書いてなくても依存関係にあるものは、書かれるのだから、今回はその他gemがnokogiriに依存しているのだろう。
今回は
bundle update
が適当だろう。
gem update
- gem コマンドは Gemfile や Gemfile.lock とは無関係に動作
- 被インストールgemについて,より新しいバージョンがあれば最新版をインストール
bundle update
- Gemfile と Gemfile.lock に基づいて動作
terminalbundle update nokogiri git add -i git commit -m 'update nokogiri' git push origin master'セキュリティアラート消えた。
- 投稿日:2019-08-29T12:15:52+09:00
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 が最新でした。悪しからず
)
$ rails --version Rails 6.0.0.rc2今回は、 Child モデルを作成して
rails console
を使って確認します。プロジェクトを作る
rails new rails_sandbox --database postgresql cd rails_sandboxChild モデルを作る
name と generation の2つの属性をもつ Child モデルを作ります。
generation は enum にするため、 integer にします。bin/rails g model Child name generation:integerenum を定義する
Child モデルに enum を定義します。
app/models/child.rbclass Child < ApplicationRecord enum generation: %i[baby toddler preschool gradeschool teen young_adult] endseed データを作成する
seed データを作成します。
db/seeds.rbChild.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:seedrails 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.rbclass Child < ApplicationRecord enum generation: %i[baby toddler preschool gradeschool teen young_adult], _scopes: false endrails 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参考情報
- 投稿日:2019-08-29T10:58:25+09:00
rails console 上で実行時間を計測する
Rails Tutorial 5日目。
現在第8章を勉強中。知りたいこと
find_by と find_by_x ってどっちが速いの?
nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。
find_by が古いタイプと言いながら、このあとの演習でも find_by がたくさん出てくるから、やっぱり古くないのではないか???(互換性があるから update されてないだけ?)追記:@scivola さんがコメントで勘違いを指摘してくださいました。ありがとうございます!
古いタイプの find_by == find_by_x ということだったのか~!やりたいこと
find_by と find_by_x ってどっちが速いのか実行時間を計測してみたい。
ほかに知らないこと
関数定義
rails_consoledef 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
- 投稿日:2019-08-29T10:27:52+09:00
Solidityでカイジのチンチロゲームを作った話
成果物
とりあえず、LightSailで簡易公開。
Metamask入れて、Ropstenネットワークを選択して遊べます!!
メインネットでは遊べないです。概要
カイジのチンチロを作りたかった・・・
Ethereumを使って。
完成形はユーザ登録とユーザの持ち金(GTIP)の量を管理するにとどまりました。Rails側で乱数生成、勝敗の決定をして
勝敗に応じてコントラクトの関数を叩いてウォレットに紐づくユーザのGTIPを動かそう!
という取り組みになりました。技術レベル
Rails(勉強したて)
Solidity(ゾンビやったくらい)
Html/CSS(ぱんぴぃ)決して綺麗なコードではないですが、勉強の軌跡として残しておきます!
Webソース
https://github.com/tokai-son/Gambreum_web
ちょっと整理できていないです。JSファイルに細かく分離したかった…Solidityの部分
GambreumPlayer.solpragma solidity ^0.4.24; import "./GambreumTip.sol"; import "../node_modules/zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; import "../node_modules/zeppelin-solidity/contracts/ownership/Ownable.sol"; contract GambreumPlayer is GambreumTip { struct PlayerInfo { string username; uint winrate; bool locked; } event PlayerCreated(string username); mapping (address => PlayerInfo) public addressToPlayerInfo; mapping (address => uint) public addressToBalance; function createPlayer(string _name) public { emit PlayerCreated(_name); require(keccak256(_name) != keccak256("username")); string memory player_name = addressToPlayerInfo[msg.sender].username; require(bytes(player_name).length == 0); addressToPlayerInfo[msg.sender] = PlayerInfo(_name, 0, false); balances[msg.sender] += 100; //初期配布分の100GTIP } function viewPlayerInfo() public view returns (string username, uint winrate, bool locked) { PlayerInfo memory player_info = addressToPlayerInfo[msg.sender]; return (player_info.username, player_info.winrate, player_info.locked); } function publishTokenToPlayer(uint value, address to) public onlyOwner { balances[to] += value; } function returnToken(uint value, address player_address) public onlyOwner { balances[player_address] -= value; } }最初やろうとしていたことからだいぶ脱線しましたw
あと、後から気づいたんですが「ERC20じゃなくてもよくね?」が、truffleを用いた一連の開発手順は勉強になりました。
「書く、デバック(今度これ読もうかな)、コンパイル、テスト、デプロイ」今回、こんな感じで作りました。
クライアント -> Local Rails -> infura -> Contract in Ropsten一番苦労した事は、トランザクションをサーバ側から叩くことです。
サーバからコントラクトのオーナーアカウントで
onlyOwner属性の関数を叩く必要があります。クライアントからコントラクト叩くのは
Web3とMetamaskちゃんが頑張ってくれました。
がサーバからトランザクションをAPI経由で叩くには…
APIに投げる「コントラクトオーナーの署名つきのRaw Transactionを作成する必要がある」コードを見るとなーんだ、と思うのですが
ここにたどり着くまでに紆余曲折。ポイントはhex_dataの部分、Ethereum ABIの形式に合わせる必要があります。
簡単に言うと、
1 関数名(引数の型スペース無、名前は省略)を256でハッシュし
その最初の4Byteだけ使います。
2 引数として渡すデータを16進数で渡す。ただし、32Byteの長さになるように0でパッティング
(3 引数が可変長なら付加情報が必要です。詳しくは、ここを見て!)
最後に、こいつらを全部繋げて先頭に”0x”をつければ完成。RawTransactionを作っています。(Ethereum.rb使いました)def exeRewardProc(amount, user_wallet, is_earn) # Create instanse from my private key whose is Gambrerum Owner. key = Eth::Key.new priv: "<秘密鍵 MetamaskからExportした>" # Get transaction count response = getMyTransactionCount() string_response = response.body json_response = JSON.parse(string_response) my_nonce = json_response["result"] # Create hex_data as a payload on this tx. if(is_earn == true) selector = Digest::SHA3.hexdigest("publishTokenToPlayer(uint256,address)", 256).slice(0..7) # 最初の4Byteを使う else selector = Digest::SHA3.hexdigest("returnToken(uint256,address)", 256).slice(0..7) # 最初の4Byteを使う end amount = amount.to_s(16) # 0xへ arg1_uint = amount.rjust(64, "0") #FIX: 10進数だから16に直す! arg2_address = user_wallet.slice(2..-1) arg2_address = arg2_address.rjust(64, "0") hex_data = "0x" + selector + arg1_uint + arg2_address tx = Eth::Tx.new({ data: hex_data, gas_limit: DEFAULT_GAS_LIMIT, gas_price: DEFAULT_GAS_PRICE, nonce: my_nonce.hex, from: "0x6CaFf8d3958dB8EF53b1Abbe10622c26DBFa4778", to: CONTRACT_ADDRESS, value: 0 }) # Sign this transaction by the key tx.sign key transaction_response = sendMyRawTransaction(tx.hex) transaction_response_string = transaction_response.body logger.debug(transaction_response_string) transaction_response_json = JSON.parse(transaction_response_string) return transaction_response_json["result"] end以下で、TransactionをAPI経由で送付しています。
def sendMyRawTransaction(signed_pay_load) uri = URI.parse(ROPSTEN_URL) request = Net::HTTP::Post.new(uri) request.content_type = "application/json" request.body = JSON.dump({ "jsonrpc" => "2.0", "method" => "eth_sendRawTransaction", "params" => [ signed_pay_load ], "id" => 1 }) req_options = { use_ssl: uri.scheme == "https", } response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http| http.request(request) end return response endなんだかんだ、サイコロ回して勝ち負け決まって
トランザクションが発行されTIPが動くとやったー!ってなりました。ただ、まだまだ改善の余地ありです。
1 TIPを動かすトランザクションが承認されるまで待たんといかん
当たり前ですが、実はもうTIPないのに次の勝負!
なんてこともできちゃいます・・・
解決策:トランザクションを発行されたらアカウントをロック
サーバ側でDBを別途用意し、トランザクションを監視2 マイナスになるくらい負けると偉いことになる
TIPの管理を下記のソースないのbalanceという変数で管理
zeppelin-solidity/contracts/token/ERC20/BasicToken.sol
〜 uint256...これがマイナスに振れた時、世界は反転する… 〜解決策:掛け金がマイナスにならない健全なギャンブルをして頂く。
and
SafeMathを使う(ゾンビで習ってた…)3 Metamaskでテストネット以外を選択している場合を検知する
ここは後から気づきました…
クライアント側で「web3.eth.net.getNetworkType」を呼んで判定させれば良さそう!BasicToken.solpragma solidity ^0.4.24; import "./ERC20Basic.sol"; import "../../math/SafeMath.sol"; /** * @title Basic token * @dev Basic version of StandardToken, with no allowances. */ contract BasicToken is ERC20Basic { using SafeMath for uint256; mapping(address => uint256) internal balances; ~ snip ~理想通りにはなりませんでしたが、一応遊べる形として完成させられました。
やはり、1から手を動かして作るのは大変ですけど、楽しいですね。今後は…
・Rspec使ってみる
・HTTPSでアクセスできるようにする
・本環境をDockerのimageにしてみる
この辺りをやってみたいと思います。
- 投稿日:2019-08-29T10:25:59+09:00
インクリメンタルサーチ+複数語検索を作る
Ruby 2.5.1
Rails 5.2.3今回作るもの
インクリメンタルサーチ + 複数語検索
スペースがある場合複数のキーワードで検索することができ、後の方の検索語でヒットするものから上部に表示します。
画面がチカチカしないように、ゆっくり表示します。画像は現在作っている、色の図鑑みたいなアプロケーションです。
検索が好きな方、インクリメンタルサーチをつけたい方のお役に立てば幸いです。
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の軽量化や正規表現について
非常に有意義なコメントをいただいたので是非お読みになってください!
せっかくなので本文はそのまま改善の余地のある見本の形で残しておくことにします!
- 投稿日:2019-08-29T09:01:36+09:00
Railsで「has been removed from the module tree but is still active!」が出た時の対処法
例
たぶんこう言うnamespece内に作ったクラスを呼ぶ時にこのエラーが起きてると思う。
# app/models/some_namespace/your_model.rb module SomeNamespace class YourModel ... ... end end通常これでRails側がちゃんとロードしてくれるが、developmentで
rails s
した状態で上のようなクラスを呼ぶファイルを更新した時にreload
が裏で走り、SomeNamespace::YourModel
が無いよ的なエラーになってしまう。解決方法
色々試してみたが、「定数読み込み順と名前空間」地獄とrequire_dependencyのやり方が一番安定してるっぽい。
# app/models/some_namespace/your_model.rb # 最初から自分でつなげておく... class SomeNamespace::YourModel ... ... end試した事
SomeNamespace::YourModel
を::SomeNamespace::YourModel
と変える(頭に::を付ける)
これはこれで動くが全部の箇所で::
を付けなければいけない- development.rbで
config.eager_load = true
(全くの見当違い)
- 投稿日:2019-08-29T06:49:15+09:00
Nuxt.js + GraphQL + Ruby on Railsで作ったアプリにJWT認証を追加する方法
本記事ではフロントエンドに Nuxt.js(Vue.js)、バックエンドに Ruby on Rails 、APIに GraphQL を採用したアプリケーションに、JWTトークンによる認証 を追加する方法についてまとめます。
題材
サンプルとして以下チュートリアルで作成したToDoリストを使用します。
Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(前編)
Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(後編)JWTでの認証フロー
今回実装するJWTでの認証フローを図にまとめました。
トークンの発行・保存・付与・検証がめんどくさそうに見えるかもしれませんが、フロントエンド側はAuth Moduleが、バックエンド側はknockがトークンをいい感じに処理してくれるので、安心してください。
実装(バックエンド)
ユーザモデルを定義する
認証単位となるモデル(User)を生成します。
$ bundle exec rails g model user email:string password_digest:string $ bundle exec rails db:migrateUserはパスワードをハッシュ化して管理するので、モデルに
has_secure_password
を宣言します。app/models/user.rbclass 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:seedknockをインストールする
Gemfileにknockを追加し、インストールします。
Gemfilegem 'rack-cors' gem 'graphql' gem 'knock' # ★追加$ bundle installRails6だとknockのautoloadに失敗するのでinitializerで明示的にrequireします。
config/initializers/eager_load_knock.rbrequire 'knock/version' require 'knock/authenticable'ジェネレータを実行します。
$ rails generate knock:install
ログイン/ログアウトのエンドポイントを準備
ジェネレータでControllerを生成します。
$ rails generate knock:token_controller user
各Controllerにて認証処理を行えるようにするため、
ApplicationController
にてmoduleをincludeします。app/controllers/application_controller.rbclass ApplicationController < ActionController::API include Knock::Authenticable end
GraphqlController
のbefore_actionとして認証処理を追加します。app/controllers/graphql_controller.rbclass 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
が返ってきます。
/user_token
へリクエストすると、JWTトークンが返ってきます。Bearerの設定でTOKEN欄に上記JWTトークンを記載します。
この状態で再度GraphQL Queryを送信すると、
200
が返ってきました。
JWT認証が機能していますね。実装(フロントエンド)
Auth Moduleをインストールする
npmでインストールします。
Auth Moduleはstore/index.js
が存在していないとエラーを出すので、空のファイルを作成しておきます。$ npm install @nuxtjs/auth @nuxtjs/axios $ touch store/index.js
nuxt.config.js
のmodules
,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: () => '' } } } // ・・・中略・・・ログイン画面を準備
/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トークンをセットしています。ログアウトボタンを追加
ログイン中の場合のみ、メニューバーにログアウトボタンを表示します。
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>© 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' // 中略 }動作確認(全体)
冒頭の画像のように、ログインしたときのみタスク一覧が表示されます。
まとめ
本記事ではバックエンドにRuby on Rails、フロントエンドにNuxt.js、APIにGraphQLを採用したアプリケーションにJWT認証を追加しました。
新規登録画面やタスクとユーザの紐付け等、未実装の処理は多数あるものの、認証というアプリケーションを実装するときの最初の壁は越えられたかと思います。knockやAuth Module、Apollo Moduleを用いたことで、トークン操作を意識せず簡単に認証追加できたので、ぜひお試しください。
- 投稿日:2019-08-29T01:22:39+09:00
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章からも頑張りたいと思う。