Railsアプリの本番環境における画像データの置き場所

こんにちは!kossyです!




さて、今回は、Railsでアプリケーションを開発する際の、
本番環境における画像データの置き場所はどこがいいのか、について、
ブログに残してみたいと思います。




ロゴや固定のアイコンはassetへ、ユーザーがアップロードする画像はpublicへ

結論は上記になります。

なぜ、配置する場所を変えるのかと言うと、asset_precompileの対象の範囲にpublicが含まれないからです。

ログや固定のアイコンと言うのは、システム側に依存するものなので、precompileの対象にさせるために、
assets配下に置きます。

対して、ユーザーがアップロードするような画像は、precompileの対象に含む必要はないので、
public配下に置かれるようにします。



asset precompileの説明はこちらのブログが詳しかったです。
Rails初学者がつまずきやすい「アセットパイプライン」



参考にさせていただいたサイト

Ruby on Rails - Railsで扱う画像はassetsとpublic、どちらに置くといいのでしょうか?|teratail

ActiveRecordのメソッドに「!」がついた時とそうでない時の挙動の違い

こんにちは!kossyです!




さて、今回は、ActiveRecordのメソッド(create, update, destroy, save)に!が付いている時と
そうでない時の挙動の違いについて、ブログに残してみたいと思います。




例外を起こすか否か

端的にまとめると、上記になります。

「!」を伴うメソッドは、バリデーションの失敗時に例外を起こし、動作を終了します。
対して、「!」を伴わないメソッドは、バリデーションに失敗しても、例外を起こさずに動作を継続させます。



トランザクション処理を行いたいとき


例えば、ActiveRecord.transaction メソッドを使うときは、ブロック内でcreateやsaveをする場合、

ActiveRecord::Base.transaction do
  @hoge.save!
end

というように、!をつけて例外を起こすようにします。
transactionは例外処理に反応してロールバックするので、!をつけて例外を起こさせるようにするわけです。



まとめ

・!の有無は、バリデーションの失敗時に例外を起こすか否か
・!が付いていれば例外を起こす
・なければ起こさない
トランザクション処理のブロック内では!を付ける




参考にさせていただいた記事

rails のtransaction処理 - Qiita
rails save! create! update!のバリデーション例外を捕捉する - Qiita

「overloaded POST」 ってなんぞや?

こんにちは!kossyです!





めっきりアウトプットが減ってしまいました、、、
Ruby sliverの取得・基本情報技術者の取得・オリアプの開発など、
やりたいこと、やるべきことが湯水の如く湧いて出てくるので、
やらないことを決めることが困難な状況であります。



今日は久しぶりにブログを描きたくなったので、
「overloaded POST」について、ブログに残してみたいと思います。




HTML5ではPUT/DELETEメソッドが採用されてない

HTML5(正確には旧バージョンでも採用されていないが)ではformでHTTPメソッドである
PUTとDELETEメソッドを採用していません。

HTTPメソッドってなんぞや?という人は、
こちらを参考にしてみてください。
https://wa3.i-3-i.info/word11405.html

平たく言うと、インターネットでWebページを見るときのお願いの仕方のことですね。
そのお願いの仕方の中に、値を更新するような処理をするPUTメソッドと、
値を削除する処理をするDELETEメソッドが採用されていないと言う訳です。

「え、削除も更新もできないってやばくね?」
って思われる方がいらっしゃるかもしれませんが、
先人達の知恵は素晴らしく、PUTやDELETEが無くとも更新や削除の挙動を実現しているのです。

その一つが、「overloaded POST」と言う手法です。




「overloaded POST」って?


webアプリケーションフレームワークの一つであるRuby On Railsでは、PUTやDELETEの挙動を、
_methodというパラメータにHTTPメソッド名(PUTとかDELETEとか)を埋め込んでPOSTリクエストをすることで、
実現しています。この手法を「overloaded POST」と呼ぶそうです。

なので、RailsでDELETEの挙動を実現させるためには、

    = link_to "削除", "/reviews/#{@review.id}", method: :delete

という風に、method: :deleteと明示的に記述する必要があります。



なんでHTML5にPUTとDELETEが採用されてないの?



jxck.hatenablog.com


こちらの記事によると、


まとめると、以下のような感じかと思います。

HTML4.x 以前: 需要があまりなく、積極的に仕様に追加する人もおらず、 HTTP3.2 からの仕様を引き継いでいた。
XForms: 仕様には取り込んだが、それを含む予定だった XHTML2.0 の策定が終了し、普及しなかった。
HTML5: 議論は何度かあり、現在 Cameron 氏のドラフトあたりが有力で、これはそのうち ML に投げられる模様。
HTML5.1?: Cameron 氏のドラフトが議論され、承認されればこの辺で入るかもしれない。入らない可能性は大いにあると思われる。

とのことで、需要がないから採用されていない、という要因が大きそうです。




参考にさせていただいた記事

なぜ html の form は PUT / DELETE をサポートしないのか? - Block Rockin’ Codes
https://wa3.i-3-i.info/word11405.html
https://www.amazon.co.jp/%E3%83%91%E3%83%BC%E3%83%95%E3%82%A7%E3%82%AF%E3%83%88-Ruby-Rails-%E3%81%99%E3%81%8C%E3%82%8F%E3%82%89-%E3%81%BE%E3%81%95%E3%81%AE%E3%82%8A/dp/4774165166

深いアソシエーションを組んだ時はdelegateを設定しよう

こんにちは!kossyです!




さて、今回はメソッドの移譲を実現するメソッド、delegateメソッドの使い方を
ブログに残してみたいと思います。




環境
Rails 5.2.2
Ruby 2.5.1
MacOS Mojave





ユースケース

例えば、
・Userクラス(name, email, profile_photo, password )
・Postクラス(title, content, user_id)
があったとして、
User has_many postsの関係とし、
特定のポストのユーザーのプロフィール写真を取得したいとします。

その場合、

@post.user.profile_photo

として値を取得すると思うのですが、
ここでdelegateメソッドを使うと、

@post.profile_photo

で値が取れるようになります。



実装手順

実装はとても簡単です。

class Post
  belongs_to :user
  delegate :profile_photo, to: :user
end

これで実装は終了です。
めちゃくちゃ簡単なのでぜひ試してみてください。

deviseのエラーメッセージ にbootstrapを当てたい

こんにちは!kossyです!




さて、今回はRailsの認証系機能を提供しているgem「devise」のエラーメッセージ に
cssフレームワークであるbootstrapを当てたい時の方法をブログに残してみたいと思います。





なお、devise及びbootstrapの導入方法については割愛してます。
このブログを読んでくださる方はエラーメッセージ の当て方について解を求めている方だと思いますので。



環境



devise_error_messages!をオーバーライド



bootstrapを適用したエラーメッセージ を表示するには、いくつかやり方がありますが、
ここでは、devise_error_messages!をオーバーライドする形を取りたいと思います。



app/helpers/application_helper.rb

module ApplicationHelper

  def devise_error_messages
    return "" if resource.errors.empty?
    errors_html = ""
    resource.errors.full_messages.each do |err_msg|
      errors_html += <<-EOF
        <div class="error_field alert alert-danger" role="alert">
          <p class="error_msg">#{err_msg}</p>
        </div>
      EOF
    end
    errors_html.html_safe
  end

end


html_safeはエスケープを行いたくない場合に使用するStringクラスのメソッドです。


ユーザー登録フォームの例はこんな感じです。(hamlで書いてます)

app/views/devise/registrations/new.html.haml

= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
  = devise_error_messages
  .form-group
    = f.label :name, "名前"
    = f.text_field :name, autofocus: true, class: "form-control"
  .form-group
    = f.label :email, "メールアドレス"
    = f.email_field :email, autofocus: true, class: "form-control"
  .form-group
    = f.label :profile_photo, "アイコン"
    = f.file_field :profile_photo, autofocus: true, class: "form-control"
  .form-group
    = f.label :password, "パスワード"
    = f.password_field :password, autofocus: "off", class: "form-control"
  .form-group
    = f.label :password_confirmation, "パスワードの確認"
    = f.password_field :password_confirmation, autofocus: "off", class: "form-control"
  .actions
    = f.submit "登録する", class: "btn btn-primary w-100"

デフォルトでは

devise_error_messages!

ですが、オーバーライドしたメソッドを使うため、!を消してます。



参考にさせていただいたサイト
RubyonRails:deviseのエラーメッセージをカスタマイズする方法 - Madogiwa Blog
[Rails]ERBのエスケープを自在に扱おうぜ - Qiita

Railsアプリにdevise-two-factorとrqrcodeを使って2段階認証を導入した

こんにちは!kossyです!




最近はオリジナルアプリの実装に熱を入れ過ぎてブログの更新が疎かになっておりました、、、
アウトプットが大事だと頭でわかってはいるものの、
実行に移すのは大変ですね、、、





さて、今回はRailsアプリに2段階認証を導入する手順について、
ブログに残してみたいと思います。




環境
Rails 5.2.2
Ruby 2.5.1
devise 4.6.1
devise_two_factor 3.0.3
rqrcode 0.10.1
dotenv-rails 2.6.0
google_authenticator
macOs Mojave



[注意]
この解説記事内でgoogleの2段階認証アプリを使うので、実際に導入してみたい方は
お手持ちのスマートフォンGoogle Authenticatorを入れて下さい。
iOS: ‎「Google Authenticator」をApp Storeで
Android: Google 認証システムのインストール - Android - Google アカウント ヘルプ





Gemの導入

まずはGemの導入からです。

./Gemfile

# groupのブロック外ならどこに記述してもOKです

gem 'devise'
gem 'devise-two-factor'
gem 'rqrcode'
gem 'dotenv-rails'

# ターミナル
$ bundle install

それぞれ端的に解説すると、
deviseは言わずと知れた、認証の仕組みを数コマンドで提供してくれるGemです。
devise_two_factorを使うと、簡単に2段階認証の仕組みをRailsアプリに導入することができます。
rqrcodeは、QRコードの生成を行うgemです。
dotenv-rails環境変数の管理をしてくれるGemです。



deviseの導入

deviseの導入は細かい説明は省きます。
詳しく知りたい方は公式ドキュメントや、
[*Rails*] deviseの使い方(rails5版) - Qiita
こちらを参考にしてみてください。

$ rails g devise:install

$ rails g devise:views

$ rails g devise User

$ rails g devise:controllers users

$ rails db:migrate

上記のコマンドを順に実行してください。

次に、ルーティングを追加します。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

end

これでdeviseの導入は終了です。

次にdevise_two_factorの設定を行います。




devise-two-factorを導入


ターミナルで以下のコマンドを実行します。

$ rails g devise_two_factor User TWO_FACTOR_ENCRYPTION_KEY

$ rails db:migrate

このコマンドを実行することで、以下の処理が実行されます。
・app/models/user.rbにdevise_two_factorの設定を追加
・:database_authenticatableを削除しようとする。 ※削除されない場合は手動で削除してください。
・config/initializers/devise.rbにwardenの設定を追加
・db/migrate下に二段階認証関連のカラムを追加するためのマイグレーションファイルを生成
追加されるカラムは以下の5種類です。

  • encrypted_otp_secret(string)
  • encrypted_otp_secret_iv(string)
  • encrypted_otp_secret_salt(string)
  • consumed_timestep(integer)
  • otp_required_for_login(boolean)

ワンタイムパスワードのパラメータを入れるカラムと、
ユーザーが2段階認証を有効にしているかどうかを判断するためのカラムを追加しています。





次に、環境変数の設定を追加します。



.envファイルの設定



アプリケーションフォルダ直下に.envファイルを作成し、
環境変数を設定します。

./ .env

# 例です
TWO_FACTOR_ENCRYPTION_KEY=hogehogehogehogehogehogehogehogehogehogehoge

このファイルは.gitignoreに追加して、gitの管理から外れるようにしましょう。

.gitignore

.env


次に、application_controller.rbにStrongParametersでotp_attempt(認証コード)を許可するように設定を加えます。


StrongParameterの設定

StrongParameterについては以下の記事が詳しかったです。
Rails初学者がつまずきやすい「ストロングパラメータの仕組み」

app/controllers/application_controller.rb



class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
  end

end

次項から、2段階認証の仕組みを実装していきます。



2段階認証を実装



Deviseを使っているRailsアプリに2段階認証を導入する - Qiita
こちらの記事のコードを参考に実装させていただきました。


app/controllers/users/sessions_controller.rbを編集し、通常のdeviseの認証の前に処理を挟むようにします。

class Users::SessionsController < Devise::SessionsController
  prepend_before_action :authenticate_with_two_factor, only: [:create]

  private
  def authenticate_with_two_factor
    # strong parameters
    user_params = params.require(:user).permit(:email, :password, :remember_me, :otp_attempt)

    if user_params[:email]
      user = User.find_by(email: user_params[:email])

    elsif user_params[:otp_attempt].present? && session[:otp_user_id]
      user = User.find(session[:otp_user_id])
    end
    self.resource = user

    return unless user && user.otp_required_for_login

    if user_params[:email]
      if user.valid_password?(user_params[:password])
        session[:otp_user_id] = user.id
        render 'devise/sessions/two_factor' and return
      end

    elsif user_params[:otp_attempt].present? && session[:otp_user_id]
      if user.validate_and_consume_otp!(user_params[:otp_attempt])
        session.delete(:otp_user_id)
        sign_in(user) and return
      else
        flash.now[:alert] = 'Invalid two-factor code.'
        render :two_factor and return
      end
    end
  end
end


次に、認証コードの入力画面を作成します。

app/views/devise/sessions/two_factor.html.erb


<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| %>
  <div class="field">
    <%= f.label :otp_attempt %><br />
    <%= f.text_field :otp_attempt %>
  </div>

  <div class="actions">
    <%= f.submit "Verify" %>
  </div>
<% end %>

ルーティングは下記のように実装します。

resource :two_factor_auth, only: [:new, :create, :destroy]


次に、2段階認証周りの機能を提供するtwo_factor_auths_controller.rbを作成します。

app/controllers/two_factor_auths_controller.rb



class TwoFactorAuthsController < ApplicationController

  # 2段階認証有効化確認
  def new
    unless current_user.otp_secret
      current_user.otp_secret = User.generate_otp_secret(32)
      current_user.save!
    end

    @qr_code = build_qr_code
  end

  # 2段階認証有効化
  def create
    if current_user.validate_and_consume_otp!(params[:otp_attempt])
      current_user.otp_required_for_login = true
      current_user.save!

      redirect_to root_path

    else
      @error = 'Invalid pin code'
      @qr_code = build_qr_code

      render 'new'
    end
  end

  # 2段階認証無効化
  def destroy
    current_user.update_attributes(
      otp_required_for_login:    false,
      encrypted_otp_secret:      nil,
      encrypted_otp_secret_iv:   nil,
      encrypted_otp_secret_salt: nil,
    )
    redirect_to root_path
  end

  private
  
  def build_qr_code
    label = "service_name"
    # issuerでGoogle Authenticator上の登録サービス名として表示
    issuer ="company_name"
    uri = current_user.otp_provisioning_uri(label, issuer: issuer)
    qrcode = RQRCode::QRCode.new(uri)
    qrcode.as_svg(
      offset: 0,
      color: '000',
      shape_rendering: 'crispEdges',
      module_size: 2
    )
  end
end






QRコード表示画面を作成

app/views/two_factor_auths/new.html.erb



<h1>Enable 2FA</h1>

<% if @error %>
  <div><%= @error %></div>
<% end %>

<div><%= raw @qr_code %></div>

<%= form_tag two_factor_auth_path do %>
  <div class="field">
    <%= label_tag :otp_attempt %><br />
    <%= text_field_tag :otp_attempt %>
  </div>

  <%= submit_tag :submit %>
<% end %>


認証機能の有効化・無効化画面の例

app/views/home/index.html.erb(ユーザープロフィール画面があればそこに表示するのがベスト?)


<div>
  <% if user_signed_in? %>
    <% if current_user.otp_required_for_login %>
      <%= button_to "Disable 2FA", two_factor_auth_path, method: :delete %>
    <% else %>
      <%= button_to "Enable 2FA", new_two_factor_auth_path, method: :get %>
    <% end %>
  <% end %>
</div>


これで一通りの実装は完了です。
それでは、ローカルで試してみます。(本ブログでは解説していないcontrollerとrootの設定をしています。)





localhost:3000/users/sign_upにアクセスし、必要情報を入力して、submitを押下します。
f:id:kossy-web-engineer:20190220220517p:plain




ユーザー登録に成功すると、enable 2FAのボタンが表示されるので、クリックします。
f:id:kossy-web-engineer:20190220220608p:plain




すると、QRコードが表示されるので、google認証のアプリでQRコードを読み取り、6桁の認証コードを入力します。
f:id:kossy-web-engineer:20190220220754p:plain




正しく認証をパスすると、Enable 2FAだったボタンが、Disable 2FAの表示に変わっていると思います。
f:id:kossy-web-engineer:20190220220854p:plain




これで2段階認証の導入は終了です。
思った通り動作するのはやっぱり嬉しいですね。




参考にさせていただいた記事
Deviseを使っているRailsアプリに2段階認証を導入する - Qiita
deviseとGoogle Authenticatorを用いてRailsシステムに「二段階認証」を導入した話 - LiBz Tech Blog
Railsの二段階認証Gem Devise-Two-FactorのDemoを触ってみた - Qiita
GitHub - tinfoil/devise-two-factor: Barebones two-factor authentication with Devise

冗長な検索ロジックはscopeにしちゃおう

こんにちは!kossyです!



さて、今回はRailsで独自にモデルのデータの絞り込みを定義できるscopeについて、
ブログに残してみたいと思います。




環境
Rails 5.1.6
Ruby 2.5.1
MacOS Mojave




実装方法



例えば、「公開が終了した映画の中で原作が漫画でないデータをidが古い順に取りたい」をモデルから絞り込みをするとします。
これをコードに落とし込むと、

@movies = Movie.where(status: :end).where_not(original_work: :comic).order(id)

ってな感じになるかと思うのですが、これをscopeで定義できまして、

class Movie < ApplicationRecord
  scope :not_comic_end_movie, -> { where(status: :end).where_not(original_work: :comic).order(id) } 
end

って感じで定義できます。scopeとして定義すると、

@movies = Movie.not_comic_end_movie

とするだけで、

  @movies = Movie.where(status: :end).where_not(original_work: :comic).order(id)

とした時と同じレコードを取得することができます。

同じような検索を何度も行うような時や、記述が冗長になってしまった時に有効な手段だと思います。

bundle installする時はパスの指定をしよう

こんにちは!kossyです!




さて、今回はbundle installする時の注意事項について、ブログに残してみたいと思います。




bundle installする時はpathを指定しよう

普段Gemfileを編集すると、
bundle installを実行すると思うのですが、

    • path vendor/bundleでパスの指定をしたほうが良いです。

理由は、


gemを任意のディレクトリにインストールし、gemをRailsプロジェクト毎に管理するためです。
ディレクトリを指定しないbundle installはシステム領域にインストールされるため、複数のrailsアプリを同一のマシン上で運用していると、問題が発生することがあります。
vendor/bundleは、アプリ専用の領域のため、他のアプリには影響がありません。

マシン上で、一つしかrailsアプリを動かさないのであれば、システム領域に入れてしまっても問題ないでしょう。

参考:
Ruby - bundle install するときになぜ vendor/bundle に入れるのか|teratail

とのことでした。


bundle install する時は余計なバグを生まないためにも
パスを指定するようにしましょう。

Rspec使う時のgenerator設定

こんにちは!kossyです!




さて、今回はrspec導入の際のgeneratorの設定についてブログに残してみたいと思います。




個人的には以下のような設定にすることが多いです。

config/application.rb


    config.generators do |g|
      g.template_engine  :haml
      g.test_framework   :rspec, fixture: false
      g.view_specs       false
      g.controller_specs false
      g.helper           false
      g.stylesheets      false
      g.javascripts      false

上記の設定ですと、
・viewはerbではなくhaml
・テストフレームワークRspecで、fixtureは生成されない
・viewのspecファイルは生成なし
・controllerもなし
・helperも
・scssファイルなし
・coffeeもなし

みたいな感じですね。

ちなみに、rails newする際に、-Tと入力して実行すると、
デフォルトで生成されるmini-testのスケルトンをスキップすることができます。

active_storageで画像を複数枚保存したい

こんにちは!kossyです!




さて、今回はRails5.2系から使えるようになった、active_storageで画像の複数枚アップロードの実装方法を
ブログに残してみたいと思います。





has_many_attachを使うだけ


active_storageの導入についてはこちらを参考にしてみてください。
開発環境でのActive_Storageの使い方 - web業界未経験からエンジニアになった人のブログ


画像を1枚添付するだけであれば、has_one_attached :image
みたいな感じで設定すれば事足りるのですが、
複数枚アップロードしたい場合は、has_many_attached :images
と記述します。




これで複数枚アップロードを実現できます。