- 投稿日:2020-12-01T23:58:35+09:00
30代からのプログラミング勉強 6日目【Udemyコース/Ruby on Rails】
本日は比較的短めのコース視聴を実施。
分からないなりにはなりますが、コースレビューの様なものを書いていきます。学習内容
【3日でできる】はじめての Ruby on Rails 4 入門(Rubyから学べる)
今回はこちらのUdemyコースを視聴。
率直な感想としては「自分にはまだ早かったけど見る価値はあった」です。1から10まで丁寧に、というよりはwebアプリケーションが出来るまでの流れを簡単な解説を挟みつつ見ていくといった印象でした。
またRuby、Railsの導入についてはターミナルへの入力を真似していけばいいだけなので分かりやすいですが、動画内で解説されているバージョンは最新のものではないので注意が必要かもしれません。
説明は
「○○という記述には××という効果があり、例えばこんな時に使います」
ではなく
「××したいので○○と書きます」
というものが多く完全に知識がない状態だと聞き慣れない単語も多かったので、ある程度勉強した後に見るといいかもしれません。所感
はじめての、とはありますが説明少なめでサクサク進んでいく感じの講座なので(フォーラムで質問しながらやる様説明はありますが)、個人的には超入門編を完了後にもう一度流れを見て復習する為に活用したいなと思いました。
ただ、Webアプリケーションがどんどん出来上がっていく様を見るのは面白かったので、あまり理解は出来なくともプログラミングってこんな感じで組み上げられていくんだなぁ〜って言うのをサラッと流し見していくのも最初の一歩としてはアリなのではないでしょうか。
- 投稿日:2020-12-01T23:35:13+09:00
【Ruby on Rails初心者向け】ログインユーザーの最新投稿を取得する方法
本のレビューアプリ作成中、ログインユーザーの最新投稿を取得する方法が複数あったので、それぞれの簡単な使い方と違いについて備忘録としてまとめました。
【前提】
- rails_6.0.0_
- mysql2
- gem 'devise'を使用してログイン機能を実装済み
【目的】
- current_userの最新投稿を取得すること
- ビューで最終投稿日時を表示すること
【結論】 : current_userに紐づく投稿を配列取得し、「.last」で取り出す
contoroller.rb@last_review = Review.where(user_id: current_user.id).lastwhereメソッドでcurrent_userに関連する投稿を取得して、lastメソッドで最後の投稿のみを切り出します。
html.erb<%= @last_review.created_at %>コントローラーで定義したインスタンス変数@last_reviewに対して、created_atカラムの情報を表示させます。
正直、自分が考えうる方法ではこの方法が最も記述が少なく、処理も早いので、今後これ以外使うことはなさそうです。
それでは、時系列順にこの結論に至った経緯を記載します。(1〜3まで)1 : 配列のまま取得して、eachで1周だけ回す
はじめに思いついたのがこの方法です。
controller.rb@last_reviews = Review.where(user_id: current_user.id).order(created_at: :desc).limit(1)whereメソッドで投稿者がcurrent_userである投稿を配列で取得
.order(created_at: :desc)の部分で降順(新しい順)に並べ替え
.limit(1)で配列の先頭1つに限定
という流れです。html.erb<% @last_reviews.each do |last-review| %> <%= last_review.created_at %> <% end %>取得した配列をeachで一覧表示します。今回は中身が一つだけなので、一回の繰り返しです。
.limit(1)で最新の投稿を取得するこの方法だと、配列ごと取得する形になるので、ビューファイルでeach文を使う必要があります。
最新の投稿5件を表示させたい時などはこの方法を使う事になりそうですが、今回は1件だけなので正直、長い&読みにくいと感じました。2 : .sliceを使用して、配列から投稿を1つだけ切り取る
controller.rb@last_review = Review.where(user_id: current_user.id).order(created_at: :desc).slice(0)先ほどの.limit(1)の部分を.slice(0)に変更して、配列から先頭の1要素を切り出します。
配列ではなく最新の投稿情報のみをインスタンス変数に定義しており、これならばeach文を用いる事なく投稿情報をビューファイルに表示する事ができます。html.erb<%= @last_review.created_at %>先ほどよりはスッキリしましたが、「引数に、切り出す配列の要素の番号や範囲を指定する」というsliceの性質上、配列の中身を降順に並べ替える必要があります。
3 : .lastを使用して、配列の末尾の要素を取り出す
controller.rb@last_review = Review.where(user_id: current_user.id).last.lastを用いる事で配列の末尾の要素のみを取り出す事ができます。
.slice(0)との違いとして、要素の範囲指定をしないので降順に並べ替えるが必要がなく、より短い&読みやすい記述になります。
(先頭の要素を取り出したい場合は.firstを使用します)html.erb<%= @last_review.created_at %>ビューファイルは先ほどと同じです。
【まとめ】
ログインユーザーの最新投稿を取得して表示させる方法を3つご紹介しました。
1. 配列のまま取得して、eachで1周だけ回す
2. sliceを使用して、配列から投稿を1つだけ切り取る
3. lastを使用して、配列の末尾の要素を取り出す今回は最新投稿の1件以外は不要、且つ、並べ替えの記述を省くために、最後に紹介した.lastを採用しました。(私が思いつかなかっただけで他にも方法は様々あると思います... )
【最後に】
個人的な感想ですが、いろいろなやり方がある中でも自分なりに最適な方法を選ぶ過程はプログラミングの楽しみの一つだなと実感しました。今後も、何か発見できたらその都度アウトプットしていきたいと思っています。
ここまでお付き合いいただき、ありがとうございました。
- 投稿日:2020-12-01T23:20:41+09:00
deviseだけで管理者を設定したい
はじめに
現在作成中のアプリでユーザーを
user
とadmin
に2つに分けたいと思い、実装してみた。前提
userのコントローラーやビューやモデルは作成済み。
管理者設定のgemは使わず、deviseのみで完結させる。adminコントローラー生成まで
deviseのconfigをいじる。
config/initializers/devise.rb/247行目# ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. config.scoped_views = true ←コメントアウトを外して、trueにするconfig/initializers/devise.rb/255行目# Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. config.sign_out_all_scopes = false ←コメントアウトを外してfalseにするターミナルで
rails g devise adminadminコントローラーを少し編集
app/models/admin.rbclass Admin < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, #:registerable, ←コメントアウトする :recoverable, :rememberable, :validatable end
- 投稿日:2020-12-01T23:14:07+09:00
自動調整ツールRubocopの導入と使い方
Rubocop
Rubocop(ルボコップ)はRubyの静的コード解析ツールです。
「インデントが揃っていない」「余分な改行・スペースがある」などの指摘をRubyStyleGuideに基づいて行ってくれます。Rubocopを導入
Gemfilegroup :development do gem 'rubocop', require: false endターミナル% bundle install % touch .rubocop.yml続いてRubocopの設定を行います。
.rubocop.ymlAllCops: # 除外するディレクトリ(自動生成されたファイル) # デフォルト設定にある"vendor/**/*"が無効化されないように記述 Exclude: - "vendor/**/*" # rubocop config/default.yml - "db/**/*" - "config/**/*" - "bin/*" - "node_modules/**/*" - "Gemfile" # 1行あたりの文字数をチェックする Layout/LineLength: Max: 130 # 下記ファイルはチェックの対象から外す Exclude: - "Rakefile" - "spec/rails_helper.rb" - "spec/spec_helper.rb" # RSpecは1つのブロックあたりの行数が多くなるため、チェックの除外から外す # ブロック内の行数をチェックする Metrics/BlockLength: Exclude: - "spec/**/*" # Assignment: 変数への代入 # Branch: メソッド呼び出し # Condition: 条件文 # 上記項目をRubocopが計算して基準値を超えると警告を出す(上記頭文字をとって'Abc') Metrics/AbcSize: Max: 50 # メソッドの中身が複雑になっていないか、Rubocopが計算して基準値を超えると警告を出す Metrics/PerceivedComplexity: Max: 8 # 循環的複雑度が高すぎないかをチェック(ifやforなどを1メソッド内で使いすぎている) Metrics/CyclomaticComplexity: Max: 10 # メソッドの行数が多すぎないかをチェック Metrics/MethodLength: Max: 30 # ネストが深すぎないかをチェック(if文のネストもチェック) Metrics/BlockNesting: Max: 5 # クラスの行数をチェック(無効) Metrics/ClassLength: Enabled: false # 空メソッドの場合に、1行のスタイルにしない NG例:def style1; end Style/EmptyMethod: EnforcedStyle: expanded # クラス内にクラスが定義されていないかチェック(無効) Style/ClassAndModuleChildren: Enabled: false # 日本語でのコメントを許可 Style/AsciiComments: Enabled: false # クラスやモジュール定義前に、それらの説明書きがあるかをチェック(無効) Style/Documentation: Enabled: false # %i()構文を使用していないシンボルで構成される配列リテラルをチェック(無効) Style/SymbolArray: Enabled: false # 文字列に値が代入されて変わっていないかチェック(無効) Style/FrozenStringLiteralComment: Enabled: false # メソッドパラメータ名の最小文字数を設定 Naming/MethodParameterName: MinNameLength: 1
Rubocop 標準設定 備考 文字列 シングルクォーテーション 式展開や文字列内にシングルクォーテーションがある場合はダブルクォーテーションを使います 変数名とメソッド名 キャメルケースで定義する Rubocopを実行するにはターミナルで以下のコマンドを実行します。
ターミナル# Rubocopを実行 % bundle exec rubocopチェック内容を自動で修正させることもできます。
ターミナル# Rubocopを実行し、check内容を自動修正 % bundle exec rubocop -aコードレビュー前にRubocopを実行すると見やすいコードになるかと思います。
- 投稿日:2020-12-01T22:12:16+09:00
プッシュ時にRSpec+Rubocopを自動でするようにCircleCIでbuildする【Rails】
はじめに
Railsでアプリを作っている初学者です。
アプリ内にCircleCIを使った自動テストを入れたい!と思い、導入してみましたので、その時に詰まったことをまとめました。同じようにアプリにCircleCIを入れたいなと思っている方の参考になれば嬉しいです!!
また、間違い等がありましたらご指摘いただけますと幸いです。
バージョン
Ruby:2.6.6
Rails:5.2.4
Mysql:5.7
環境構築にDocker使用前提
以下は題名のことをしたい場合&CircleCIを理解する上で必要です!
・Dockerを使用したことがある
・Rspecを導入済
・Rubocopを導入済
・Githubアカウントがあるまず、CircleCIの公式入門ガイドをすることをおすすめします!自分は実際の挙動があまりイメージ出来なかったのですが、これでざっくり理解することができました。
CircleCI入門ガイド:https://circleci.com/docs/ja/2.0/getting-started/
そして入門ガイドをしたところで、CircleCIをアプリに加えてみます。
ルートディレクトリに .circleci フォルダを作成してコンフィグを作る。
そしてデータベースを作る、とできるはず!うまくいかなかったパターン
参考:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2fまず、こちらのQiitaの記事を参考にしましたが、自分の環境ではうまくいきませんでした。
ですが、こちらの経緯も記録しておきます。config.ymlを作る
.circleci\config.ymlversion: 2.1 orbs: ruby: circleci/ruby@1.1.0 jobs: build: docker: - image: circleci/ruby:2.6.6-node-browsers environment: BUNDLER_VERSION: 2.1.4 steps: - checkout - ruby/install-deps test: parallelism: 3 docker: - image: circleci/ruby:2.5.1-node-browsers environment: DB_HOST: 127.0.0.1 RAILS_ENV: test BUNDLER_VERSION: 2.1.4 - image: circleci/mysql:5.7 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'true' MYSQL_ROOT_HOST: '%' steps: - checkout - ruby/install-deps - run: mv config/database.yml.ci config/database.yml - run: name: Wait for DB command: dockerize -wait tcp://localhost:3306 -timeout 1m - run: bundle exec rake db:create - run: bundle exec rake db:schema:load # Run rspec in parallel - ruby/rspec-test - ruby/rubocop-check workflows: version: 2 build_and_test: jobs: - build - test: requires: - build引用:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2fdatabase.yml.ciを作る
すでにconfig内にdatabase.ymlがあるけど、それとは別で新規で作ります。
新規のDockerfileを使うのでデータベースも新しくなります。config\database.yml.citest: adapter: mysql2 encoding: utf8 pool: 5 username: 'root' port: 3306 host: '127.0.0.1' database: ci_test引用:CircleCIでSystemSpec(RSpec)とRubocopを走らせる
https://qiita.com/YK0214/items/bbed63ea7ca5367dae2fGithubへプッシュして、マージするとCircleCIで自動でBuildされます
失敗している!!!Bundleのバージョンが違うみたい。
ちょっと変更
.circleci\config.ymlversion: 2.1 orbs: ruby: circleci/ruby@1.1.0 jobs: build: docker: - image: circleci/ruby:2.6.6-node-browsers environment: BUNDLER_VERSION: 1.17.2 steps: - checkout - ruby/install-deps test: parallelism: 3 docker: - image: circleci/ruby:2.5.1-node-browsers environment: DB_HOST: 127.0.0.1 RAILS_ENV: test BUNDLER_VERSION: 2.1.4 - image: circleci/mysql:5.7 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'true' MYSQL_ROOT_HOST: '%' steps: - checkout - ruby/install-deps - run: mv config/database.yml.ci config/database.yml - run: name: Wait for DB command: dockerize -wait tcp://localhost:3306 -timeout 1m - run: bundle exec rake db:create - run: bundle exec rake db:schema:load # Run rspec in parallel - ruby/rspec-test - ruby/rubocop-check workflows: version: 2 build_and_test: jobs: - build - test: requires: - build
BUNDLER_VERSION: 1.17.2
に変更して再Pushこれでできた!!!と思ってshow all checkをクリックして確認すると、
途中でbuildが止まっています...!よくよく見たらこんな記載が。
これは有料枠にしろということですね!?
無料枠ではコンテナ1つでないと動かないのですが、このコードは2つのDockerを起動させようとしています。
なので途中で止まっていたんだなあということがわかりました...というわけで修正を行います
うまくいったパターン
今度はこちらの記事を参考に、config.ymlを作成します。
エラーがいくつか出たので最終的には細かく修正を加えています。参考:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788.circleci/config.yml# CircleCIのバージョンのことで、「2」、「2.0」、「2.1」のうちのどれかを指定します。 version: 2.1 orbs: ruby: circleci/ruby@1.1.0 # jobsは、実行処理の単位です。 jobs: build: # DockerでRubyのイメージを作成します。Dockerfileもしくはローカル環境とVersionは合わせること。 docker: - image: circleci/ruby:2.6.6-node-browsers environment: # Dockerで作った、circleCI上で'bundle install'します BUNDLER_VERSION: 1.17.2 steps: - checkout - ruby/install-deps test: parallelism: 3 docker: - image: circleci/ruby:2.5.1-node-browsers environment: DB_HOST: 127.0.0.1 RAILS_ENV: test BUNDLER_VERSION: 2.1.4 - image: circleci/mysql:5.7 command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'true' MYSQL_ROOT_HOST: '%' steps: - checkout - ruby/install-deps - run: mv config/database.yml.ci config/database.yml - run: name: Wait for DB command: dockerize -wait tcp://localhost:3306 -timeout 1m - run: bundle exec rake db:create - run: bundle exec rake db:schema:load # Run rspec in parallel - ruby/rspec-test - ruby/rubocop-check workflows: version: 2 build_and_test: jobs: - build - test: requires: - build引用:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788公式:CircleCI-Public/circleci-demo-ruby-rails
https://github.com/CircleCI-Public/circleci-demo-ruby-rails/blob/master/config/database.yml参考:【CircleCI】Railsアプリに導入(設定ファイルについて)
https://qiita.com/Daiki-Abe/items/d90599d904c2b1370d10というわけで修正
①bundle exec rake:db migrateを追加
マイグレーションされていないというエラーが出ているので、上記のファイルの中に
bundle exec rake:db migrate
を追加します②細かすぎるgemまで見ているのでRubocopを編集
migrateが無事されましたが、今度はRubocopでエラーがでました。
エラー文は以下。For more information: https://docs.rubocop.org/rubocop/versioning.html vendor/bundle/ruby/2.6.0/gems/msgpack-1.3.3/.rubocop.yml: Lint/Eval has the wrong namespace - should be Security Error: RuboCop found unsupported Ruby version 2.3 in `TargetRubyVersion` parameter (in vendor/bundle/ruby/2.6.0/gems/msgpack-1.3.3/.rubocop.yml). 2.3-compatible analysis was dropped after version 0.81. Supported versions: 2.4, 2.5, 2.6, 2.7, 3.0Rubocop.ymlでrubyバージョンの指定をすることで解決しました
AllCops: # Rubyバージョン指定 TargetRubyVersion: 2.6参考:原因と対応「Error: RuboCop found unsupported Ruby version 2.1」
https://blog.mothule.com/ruby/rubocop/ruby-rubocop-found-unsupported-ruby-versionbuildには失敗してるけどRubocopは通っています!
次はRSpecですがここでもエラーが出ています。
③RSpecのmysql2::Error::ConnectionError:を解消する
②のエラーを直したあとにrspecを実行すると、エラーがでるようになりました
rspecの実行結果は以下です。PS C:\Users\> rspec An error occurred while loading ./spec/features/toppage_spec.rb. Failure/Error: ActiveRecord::Migration.maintain_test_schema! Mysql2::Error::ConnectionError: Unknown MySQL server host 'db' (0) # ./spec/rails_helper.rb:28:in `<top (required)>' # ./spec/features/toppage_spec.rb:1:in `<top (required)>' Top 0 slowest examples (0 seconds, 0.0% of total time): Finished in 0.00009 seconds (files took 3 minutes 27 seconds to load) 0 examples, 0 failures, 1 error occurred outside of examplesdatabese.ymlに以下を加えることで解決しました。
databese.ymltest: <<: *default database: Music-record_test host: <%= ENV['MYSQL_ROOT_HOST'] || '127.0.0.1' %>参考:ローカル環境のCircleCIでMysql2::Error::ConnectionError: Unknown MySQL server host 'db' (-2)が出る
https://qiita.com/mitsu1208g/questions/c9681d1204a1b878f987④RSpecのエラー LoadError: cannot load such file -- rspec_junit_formatterの解消
次はCircleCIでのRSpecでエラーがでました。
Error reading historical timing data: file does not exist Requested weighting by historical based timing, but they are not present. Falling back to weighting by name. No examples found. bundler: failed to load command: rspec (/home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec) LoadError: cannot load such file -- rspec_junit_formatter /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/lib/rspec/core/formatters.rb:235:in `require' /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core- ・ ・ ・ /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/lib/rspec/core/runner.rb:45:in `invoke' /home/circleci/repo/vendor/bundle/ruby/2.6.0/gems/rspec-core-3.9.3/exe/rspec:4:in `<top (required)>' /home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec:23:in `load' /home/circleci/repo/vendor/bundle/ruby/2.6.0/bin/rspec:23:in `<top (required)>' Exited with code exit status 1 CircleCI received exit code 1 Uploading test results 0s Uploading artifacts 0s
LoadError: cannot load such file -- rspec_junit_formatter
この1文がエラーの原因のようです。
rspec_junit_formatter
がないということで以下の記事を参考にGemを追加します。参考:CI/CD環境を作った時に起きたエラー備忘録[Rails + CircleCI +Capistrano +AWS]
https://qiita.com/orangeupup90027/items/6f83850d1842adf3c09a① rspec_junit_formatterがないと言われる
テストを走らせた時に出たエラーError reading historical timing data: file does not exist
Requested weighting by historical based timing, but they are not present. Falling back to >weighting by name.
No examples found.
bundler: failed to load command: rspec (/home/circleci/circleci-demo-ruby->rails/vendor/bundle/ruby/2.5.0/bin/rspec)
LoadError: cannot load such file -- rspec_junit_formatter結論としてはrspec_junit_formatterをgemfileに追加する。
Gemfileに
rspec_junit_formatter
を追加します。Gemfilegroup :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome # gem 'chromedriver-helper' gem 'rspec_junit_formatter' #追加 gem 'webdrivers', '~> 3.0' endこれでGithubにプッシュするとCircleCIの画面はこのようになりました。
長くなりましたが、これでCircleCIでBuildができました!
最終的なコード
細かな修正を入れた結果が以下です。
※CircleCIはインデントに厳しい&文字列のブロックの別れ方がシビアなので、実際にコードをコピー・記載するときはコメントなしの方が良いです。
circleci\config.yml# CircleCIのバージョンのことで、「2」、「2.0」、「2.1」のうちのどれかを指定します。 version: 2 jobs: build: docker: - image: circleci/ruby:2.6.6-node-browsers environment: - RAILS_ENV: 'test' - MYSQL_HOST: 127.0.0.1 - MYSQL_USERNAME: 'root' - MYSQL_PASSWORD: '' - MYSQL_PORT: 3306 - image: circleci/mysql:5.7 environment: - MYSQL_ALLOW_EMPTY_PASSWORD: 'true' - MYSQL_ROOT_HOST: '%' working_directory: ~/repo steps: - checkout - restore_cache: keys: - v1-dependencies-{{ checksum "Gemfile.lock" }} - v1-dependencies- - run: name: install dependencies command: | bundle install --jobs=4 --retry=3 --path vendor/bundle - save_cache: paths: - ./vendor/bundle key: v1-dependencies-{{ checksum "Gemfile.lock" }} - run: mv config/database.yml.ci config/database.yml - run: bundle exec rake db:create - run: bundle exec rake db:migrate - run: bundle exec rake db:schema:load - run: name: RuboCop command: bundle exec rubocop - run: name: RSpec command: | mkdir /tmp/test-results TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ --format progress \ $TEST_FILES - store_test_results: path: /tmp/test-results - store_artifacts: path: /tmp/test-results destination: test-resultsconfig\database.yml.citest: &default adapter: mysql2 encoding: utf8 pool: 5 username: <%= ENV.fetch("MYSQL_USERNAME") %> password: <%= ENV.fetch("MYSQL_PASSWORD") %> host: <%= ENV.fetch("MYSQL_HOST") %> port: <%= ENV.fetch("MYSQL_PORT") %> database: ci_testここまでお付き合いいただきありがとうございます!
参考
【circleCI】Railsアプリでgithubと連携してrubocopとrspecテストを走らせる
https://qiita.com/AK4747471/items/b2161784065f21cd1645CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
https://qiita.com/naota7118/items/056770bcf53136e94788【CircleCI】Railsアプリに導入(設定ファイルについて)
https://qiita.com/Daiki-Abe/items/d90599d904c2b1370d10#databaseymlci%E3%82%92%E4%BD%9C%E6%88%90【初心者向け】deviseのユーザー登録をRSpecでテストする
https://qiita.com/HiMinmeg/items/37a59cd266c63330797a#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B32!
- 投稿日:2020-12-01T21:28:07+09:00
【Rails】初心者向け!データベースで困った時の味方!
始めまして!
adventカレンダー3日目を担当させていただきます。
DMM WEBCAMPでメンターをしている吉村です!よろしくお願いします!
はじめに
Rails始めたばかりの頃、データベースがらみが全く理解できず、
よく夜遅くまで、悩まされたことを覚えています。。。
今回はそんなRails初心者の方向けに、
データベースとはなんぞや!といった仕組みから
具体的なコマンドを一挙に説明・紹介していきたいと思います!Railsのデータベースってどんな構造なの?!
Railsのデータベースは大きく2つのファイルから成り立っています。
マイグレーションファイルとスキーマファイル(schema.rb)です。まずは、マイグレーションとはなんなのかから解説していきたいと思います。
マイグレーションとはマイグレーションは、データベーススキーマの継続的な変更 (英語) を、統一的かつ簡単に行なうための便利な手法です。
引用:Railsガイドよくわかりませんよね笑
本来であれば、テーブルを作成するには、SQL文を直接実行して作成する必要があります。
カラムを追加したりする場合も同じです。
対して、マイグレーションを使う場合は、テーブルを新規作成するためのマイグレーションファイルを作成し、実行します。
カラムを追加する際も同じようにマイグレーションファイルを作成します。マイグレーションのイメージ
マイグレーションを超簡単に図解すると、
このようになります。
家に例えると、マイグレーションファイルは、家をどんな構造にするかを示した設計図のようなものです。
そして、rails db:migrate
と言うコマンドを実行することで、その設計図に従って家が建てられます。なので、schemaに記述されているものが実際に作られたデータベースの形になります。
※実際はschemaに直接データが保存される訳ではないですが、今回は初心者向けの記事になるのでわかりやすいよう家と表現しています。
逆に家を壊すコマンドもあります。それが、rails db:rollback
と言うコマンドです。
これを実行すると家が壊すことができます。
この辺のコマンドの使い方はあとで詳しく説明します!マイグレーションの利点
一見マイグレーションと言う機能は面倒に見えますが、この機能を使うことで特定のデータベースの文法に依存せずにテーブルへ行なう操作を記述でき、変更を実施したり元に戻すといった作業が簡単に行なえるようになります。
マイグレーションで使えるコマンド
では、マイグレーションの基礎知識がわかったところで、具体的にどんなコマンドがどんな場面で使えるのかを説明していきます。
rails db:migrate
このコマンドは一番基本的なコマンドになります。
先ほど少し書いた通り、このコマンドを実行することで初めてマイグレーションファイルをもとにデータベースが作成されます。
そして、どんな形のデータベースができたかが確認できるファイルがスキーマファイルになります。
皆さんにも馴染みのあるコマンドだと思います。== 20200818105133 CreateOrderDetails: migrating =============================== -- create_table(:order_details) -> 0.0033s == 20200818105133 CreateOrderDetails: migrated (0.0034s) ====================== == 20200819150420 CreateAddresses: migrating ================================== -- create_table(:addresses) -> 0.0066s == 20200819150420 CreateAddresses: migrated (0.0068s) =========================実際にコマンドを打ち込んでみるとこんな処理が走るはずです。
create_table
と言う処理が確認できますね。
これでテーブルが正常に作成できたことがわかります。rails db:migrate:status
とりあえず、データベース関連で困ったときはこのコマンドを打ち込みましょう!
このコマンドは今どのマイグレーションファイルまで、rails db:migrate
が行われているかを確かめるコマンドです。Status Migration ID Migration Name -------------------------------------------------- up 20200729160256 Devise create end users up 20200729160307 Devise create admins up 20200812043150 Create items up 20200812045123 Create genres up 20200817101827 Create cart items down 20200818103251 Create orders down 20200818105133 Create order details実際にコマンドを打ち込むと、このように今あるマイグレーションファイルの一覧が出てきます。
ここで注目したいのが一番左側にあるStatus
の部分です。
ここがupになっていると言うことは、rails db:migrate
がすでに実行されている状態を表します。
ので、設計図(マイグレーションファイル)をもとに家(schema)が建っている状態ですね。反対に、downになっていると言うことは、
rails db:migrate
がまだ実行されていない状態を表します。
ので、まだ設計図を作っただけの状態です。
マイグレーションファイルを編集する時は、Statusがdownになってることを確認してから行いましょう。(理由は後述します。)rails db:rollback
マイグレーションファイルを作成し
rails db:migrate
をした後(Statusがup)に、「カラムを追加したい」「テーブルの中身を書き換えたい」と思った時に使えるコマンドです。
カラムを追加したい時は、直接マイグレーションファイルやスキーマファイルを編集すればいいんじゃないかと思うかもしれませんが、ここで注意です!
まず、スキーマファイルはマイグレーションファイルに従って作られるものなので、基本的に直接書き換えることはないです!
次にマイグレーションファイルを編集すると言う方法なのですが、これは可能です!ただし、rails db:migrate
される前(Statusがdown)であることが条件です。
rails db:migrate
をした後(Statusがup)の時にマイグレーションファイルを書き換えても設計図だけ書き換わっただけの状態になり実際のデータベースの形は変わりません!
なので、一度Statusをdownにする必要があります。
それをするためのコマンドがこのrails db:rollback
になります。== 20200819150420 CreateAddresses: reverting ================================== -- drop_table(:addresses) -> 0.0038s == 20200819150420 CreateAddresses: reverted (0.0053s) =========================実際にコマンドを打ち込んでみるとこんな処理が走るはずです。
drop_table
と言う処理が確認できますね。
これでテーブルが正常にもとに戻すことができたことがわかります。
この状態になって初めてマイグレーションファイルを書き換えることができます。
さらに、通常rails db:rollback
は一番最近マイグレイションされたファイルを1つだけもとに戻すものなのですが、
rails db:rollback STEP=○
と任意の数字を指定してあげると、一気に2つ以上のファイルをもとに戻してくれたりもします。
※チーム開発をしてる時に、rollbackを使ってマイグレーションファイルを書き換えるとコンフリクトが起こりますので、注意してください。チーム開発の場合はマイグレーションファイルを新たに作成してカラムを追加したり、削除したりしましょう。
参考:マイグレーションファイルの作成、変更、削除までrails db:migrate:reset
このコマンドはデータベースを一度全てリセットして、マイグレーションファイルをもとに再度作り直すコマンドです。
似たようなコマンドにrails db:reset
がありますが、これはスキーマファイルをもとにデータベースを作り直すコマンドで、今あるデータベースの形のままデータだけをリセットしたい時に使います。
基本的にはrails db:migrate:reset
を実行してもらえば間違い無いです。最後に
いかがだったでしょうか。
マイグレーションファイルやらスキーマファイルやら、カタカナや英語がいっぱい出てきて混乱したかと思いますが、少しずつデータベースにも慣れていきましょう。
データベースは直接書き換えるよりかは、コマンドで実行することが多いので、不安になる気持ちもありますが、適切なコマンドを適切なタイミングで使えるようになると、もっと楽しくなると思います!それでは、失礼します!
- 投稿日:2020-12-01T20:28:09+09:00
バリデーション不足によるエラー解決 ~Active Storage を用いた画像投稿機能の実装時発生~
はじめに
・投稿者は初学者ですので誤った情報を投稿してしまうことがあります。その時はぜひ、遠慮会釈なしにご指摘いただけると幸いです。
今回のエラーの原因
画像データのバリデーションが欠けていたため、本来であれば画像とセットで投稿しないといけないが、画像がない状態でデータの保存ができてしまっていた。
そのため、画像のないデータを受け取ったビューファイルが混乱し、エラーが起きた。以下、詳細。
画像投稿の実装の手順
画像投稿機能を実装しようと
ActiveStorage
を導入した。
そして、以下の手順で準備を進めた。
まずは投稿データと画像データが1対1の関係になるように記した。models/prototype.rbclass Prototype < ApplicationRecord belongs_to :user has_one_attached :image #1対1の関係になる has_many :comments, dependent: :destroy validates :title, :catch_copy, :concept, presence: true end次に、コントローラーを以下のように書き換えた。
controllers/prototypes_controller.rbdef create @prototype = Prototype.create(prototype_params) if @prototype.save redirect_to root_path else render :new end end private def prototype_params params.require(:prototype).permit(:title, :catch_copy, :concept, :image).merge(user_id: current_user.id) end
prototype_params
に:image
を追加することによって投稿データに画像も付属されるように設定。続いてviweファイルの調整を行う。
views/prototypes/index.html.erb<div class="card__wrapper"> <%= render partial: 'prototype', collection: @prototype, locals: { prototype: @prototype } %> </div>views/prototypes/_prototype.html.erb<div class="card"> <%= link_to image_tag(prototype.image, class: :card__img ), prototype_path(prototype.id) %> <div class="card__body"> <%= link_to prototype.title, prototype_path(prototype.id), class: :card__title %> <p class="card__summary"> <%= prototype.catch_copy %> </p> <%= link_to "by #{prototype.user.name}", user_path(prototype.user.id), class: :card__user %> </div> </div>エラー発生
投稿の内容は、「投稿物の名前」、「キャッチコピー」、「コンセプト」と先ほど設定した「画像」の全部で4つ。
ローカル環境で動作確認をしていたところ、画像がない状態で投稿すると、投稿が弾かれることなく以下のようなエラー画面が発生した。よく見ると上から3行目に、
Can't resolve image into URL: to_model delegated to attachment, but attachment is nil
と書かれていることが分かる。
Can't resolve image/attachment is nil と書かれているので、「画像がないからエラーが出ているぞと」言っているのだろうと理解できた。エラー解決のための仮説
エラー発生直前に行なっていた動作と、エラー画面を合わせて考えると、以下のような仮説にたどり着いた。
1、画像だけがない状態で投稿をしたらエラーが発生した。
2、エラー画面に表示されているコードはビューファイルのもので、尚且つ、画像を表示するコードを赤く示している。=>あるはずの画像データがないため、ビューファイルのコードが混乱しているのではないか。
=>画像だけがない状態で投稿をしたら、弾かれなかったということは画像データのバリデーションが正常に機能していないのではないか。この2つの仮説をもとに、モデルのファイルを覗いてみると、案の定、画像データのバリデーションが欠けていた。
誤
models/prototype.rbclass Prototype < ApplicationRecord belongs_to :user has_one_attached :image #1対1の関係になる has_many :comments, dependent: :destroy validates :title, :catch_copy, :concept, presence: true end正
models/prototype.rbclass Prototype < ApplicationRecord belongs_to :user has_one_attached :image #1対1の関係になる has_many :comments, dependent: :destroy validates :title, :catch_copy, :concept, :image, presence: true end
validates :image
が抜けていたために起きたエラーであった。
これによりバリデーションがかかり、エラーの原因は解消できた。エラー画面が消えない(余談)
バリデーションを設定し直したことで原因は解消されたが、サーバーやPCを再起動してもエラー画面から抜け出すことができなった。最初は、他に抜けているところがあるに違いないと、様々なファイルを覗いたが間違いはないように思えた。
頭を抱えて休日の半日を過ごしたが、原因は簡単なものだった。バリデーションの設定前に保存した、画像がない投稿データがあるからエラー画面から抜け出せないだけであった。
Sequel Proを用いて直接、画像のない投稿データをレコードごと削除すると、無事にエラーの解消ができた。
Herokuでデプロイした後であっても、下記のブログを参考にすれば解決できることも判明したので、忘れないようにこれも記述する。参考文献
herokuでデプロイしたDBにSequelProで接続してみる
【Active Storage】ファイルアップロード時のバリデーション設定
【初心者向け】RailsのActive Recordの解説&メソッドまとめとても参考になりました。
ありがとうございました。感想
「バリデーション抜け」も「画像がない投稿データ」も、些細な原因ではあるが、解決までに十数時間を費やした。
エラー画面を見続けて、苦しさとか悲しさとか色々と感情が込み上げて泣きそうにもなったが、解決できたときは晴れやかな気持ちになって「プログラミング最高!」と1人で盛り上がった。
エラーで挫けそうになることはこれから先、何度もあるはずだが、解決できたら最高な気分になれるという今回の教訓を思い出して乗り越えたい。
- 投稿日:2020-12-01T19:22:01+09:00
学び直し1日目 環境構築、ログイン機能
1日目は初歩の初歩から、
- 環境構築
- ログイン機能
上2つをやっていきます。もう何度もやりましたが、
「できるけど理解はしていない」状態なのでやり直します!今日の教科書
神戸プログラミングアカデミーrailsチュートリアル↑のリンクを参考にやっていきます。
環境構築
この章の学習ページ
Ruby on Rails ハンズオン勉強会①メモ
特に注意することはなかった。
気になったポイント
bundlerとは?
学習ページ
【Rails】結局bundlerって何?bundlerの仕組みを図解形式で徹底解説gemのバージョンやgemの依存関係を管理してくれるgem…らしい。
バージョン確認コマンドbundler -vGemfileとGemfile.lockの違い
【Rails】結局bundlerって何?bundlerの仕組みを図解形式で徹底解説
Gemfileはアプリで利用するgemの一覧が載っているファイルで、アプリケーションディレクトリの直下にある。これを参照してgemをインストールする。
Gemfile.lockはアプリで利用するバージョンも含めた全てのgem一覧が載っているファイル。アプリで利用するgemのバージョンをロックしている。
インストールしたgemのバージョンの違いによって開発環境が異なるのを防ぐためにロックしている。
インストールされる前と後ということは覚えておこう。bundle installと bundle updateの違い
【Rails】結局bundlerって何?bundlerの仕組みを図解形式で徹底解説
bundle install
はGemfileからgemをインストール
Gemfile.lockを先に確認して、足りないものを入れる。
bundle update
はGemfile.lockに書かれている内容を無視してgemをインストール
低いGemfile.lockのバージョンを全てupdateしてくれる。
Gemfileに記載されているgemを全てアップデートするので本来ならロックがかかっていて互換性が保たれているがなくなってしまう。
アプリが動かなくなったりするので多用しない。単体でupdateするときは
bundle update gem名
ログイン機能
この章の学習ページ
[初心者] deviseを使ってログイン機能を実装してみようつまづきポイント
mysqlを利用したアプリ作成で不具合
rails new login_app -d mysqlこれをやってインストール中に
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.一部のgemに不具合が起きて結果
An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling. In Gemfile: mysql2 run bundle binstubs bundler Could not find gem 'rails (~> 6.0.3, >= 6.0.3.3)' in any of the gem sources listed in your Gemfile. run bundle exec spring binstub --all bundler: command not found: spring Install missing gem executables with `bundle install` rails webpacker:install Could not find gem 'rails (~> 6.0.3, >= 6.0.3.3)' in any of the gem sources listed in your Gemfile. Run `bundle install` to install missing gems.railsとmysqlのバージョンが合わないみたい…。
アプリのディレクトリに移動してbundle installするとまたエラー
Installing mysql2 0.5.3 with native extensions Gem::Ext::BuildError: ERROR: Failed to build gem native extension. 中略 An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.言われた通り
gem install mysql2 -v '0.5.3'その後、また
bundle install
しても同じエラーを繰り返し。
調べてどこかで見つけた下のコードを打ち込みました。
(出典メモするのわすれました…調べればすぐ見つかります。)bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"これで
bundle install
が通りました。
多分、gemfileに書かれた記述を内部で反映させてくれるコードか何かだったと思います。rails s でサーバーが立てられない
Webpacker configuration file not found /Users/daiho0323/rails-1/login_app/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/daiho0323/rails-1/login_app/config/webpacker.yml (RuntimeError)言われた通り
rails webpacker:install
でインストールWebpacker successfully installed ? ?長いロードの末に完了
しかし今度は
Mysql2::Error::ConnectionError (Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)):
だとさ、この障害は以前経験したことがあったので
sudo mysql.server start
でmysqlを起動します。直りました!
- 投稿日:2020-12-01T18:30:07+09:00
【備忘録】tapについて少し調べてみた。
環境
Ruby 2.6
Rails 5.1.7背景
機能実装中、似たような処理を探していたら「tap」を見つけてなんだこれ、と思い調べた。これは初めて遭遇したので備忘録として。
tapとは
"foo".tap {|str| puts str.reverse} # oof # => "foo"メソッドの返り値は変わっておらず小文字のままだけれど、tap自体は動いていて文字列が反対になっている。
調べたところによると、tapはメソッドの中で何かしたいけど、メソッド自体の評価は変えたくない時に使うみたい。実際に見たコード
こんな使い方もある(らしい)。
... User.new.tap do |item| item.name = "Taro" item.address = "test@gmail.com" --- 中略 --- item.tel_num = "09012345678" end.save! ...
end
の後にsave!ってなんやねんと思ったけど大分スッキリ書けますね。
tapを応用していって可読性の高いコーディングしていきたいと思います。
- 投稿日:2020-12-01T16:12:09+09:00
capistranoでデプロイするときにgitのエラー
よくあるデプロイコマンド
$ cap staging deploy
でエラー、あまり良く見たこと無いメッセージ、しかもついこの間までデプロイできてたのに…00:07 git:update 01 git remote set-url origin git@bitbucket.org:hogehoge/hoge.git ✔ 01 hoge@foo.com 0.008s 02 git remote update --prune 02 Fetching origin 02 error: rev-list died of signal 11 02 error: rev-list died of signal 11 02 error: bitbucket.org:hogehoge/hoge.git did not send all necessary objects 02 02 error: Could not fetch origin
なんじゃこりゃ。エラーメッセージでいろいろググってもあまりこれ!って解決方法は出てこない。
・リモートでgitを再インストール
・gemを全部アップデートで解決した。アップデートで解決するやつってエラーわかりにくいわ…
- 投稿日:2020-12-01T15:58:23+09:00
Windows・macOS完全対応GitLab開発環境最終版 (2020年12月)
わずか3ヶ月前にmacOSにGitLab開発環境セットアップ (GDK) (2020年9月)という記事を書きました。が、1四半期が経過し状況も変化したので新たに書くことになりました。
tl; dr: GitLab本体やGitaly(バックエンド)をmacOSやWindows、Linuxの各種環境で開発する環境を用意する。
GitLab Development Kit
GitLab Development Kitと呼ばれるGitLab本体を手元のコンピュータで開発できるツールがGitLab開発開始直後より存在していました。
一方、AWSが3年前の今日2017年12月1日(日本時間)に発表したAWS Cloud 9、マイクロソフト傘下GitHubのGitHub Codespaces (現在Early access program中)(旧マイクロソフトVisual Studio Codespaces)(移行ニュースリリース)、そして2020年10月、Google Cloudから発表された新Cloud Shell Editorに代表されるトレンドがありました。
この四半期の間にGDKがクラウドIDEプラットフォームに対応しました。対応しているクラウドIDEはGitpodというサービスです。Gitpod
https://www.gitpod.io/
GitpodはGitHub(.com)、GitLab(.com)およびBitbucket(.org)に対応するクラウドIDEサービスです。Eclipse Foundation参加のオープンソースプロジェクトEclipse Theiaをベースにしています。2020年8月にはGitpodそのものもオープンソース化されました。Gitpodクラウド版のユーザとしては、無料から始めることができ、無料プランではパブリックリポジトリであれば月50時間までワークスペースを利用できます。USおよびEUのregionにあるGCP上のKubernetes (GKEなのではないかと推測) に構築されており、ユーザーは16vCPU、59GBメモリの利用ができるようです。なお、以前にあった(そしてドキュメントに記載されている)Asiaのクラスターは既に削除されていて利用できないとのこと。
手順
ここを読むほとんどの人は以下の作業が必要になる。例外はGitLab team memberおよび過去にGitLabへコード貢献があった人(過去のGitLab Hackathon参加者も該当する可能性が高い)。
- https://gitlab.com にログインする。アカウントがない場合はSign upから作成する。
- https://gitpod.io にログインする。アカウントがない場合はGitLabアカウントでアカウント作成&ログインする。
- https://gitlab.com/gitlab-org/gitlab を開いて、Forkボタンを押したのち、自分のユーザ名の右にあるSelectボタンを押す。そして、しばらく待つ(2-10分くらい?)。
- 完了したら、表示されたページのWeb IDEの右にある▽を押し、プルダウンメニューからGitpodを選ぶ。そして、もう一度Gitpodを押す。
- 7-8分待つ。
Gitpodに関する注意
GitLabログインを通じて、Gitpod側にはクレデンシャルが設定済みなので、git fetch/push/pullの操作が可能になります。gpコマンドとして利用可能です。
GDKターミナルでは/workspace/gitlab-development-kit
がカレントディレクトリとして指定されており、gdk
コマンドが利用できます。
GitLabターミナルでは/workspace/gitlab
がカレントディレクトリとして指定されており、(通常のGDKセットアップと異なり)gdk
コマンドが利用できませんので、注意してください。その他は https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/gitpod.md を見てください。
まとめ
すぐにGitLabの開発に参加できるGitLab Development Kit powered by Gitpodについて説明しました。一人でも多くのコードコントリビュータが増えることを願っています。
すぐに試したい?
GitLab Tokyoコミュニティは12月2〜3日Hackathonイベントを日本語・オンラインで実施します。参加するには、こちらの記事に書かれたセットアップが必要となります。開催時間中にセットアップすることも可能です。ぜひご参加ください。
- 2020-12-02 (水) https://gitlab-jp.connpass.com/event/189496/
- 2020-12-03 (木) https://gitlab-jp.connpass.com/event/189683/
Qiitaの先人たちによる同種の記事
- 投稿日:2020-12-01T15:36:10+09:00
【保存版】個人開発の進め方 -全5ステップ-
はじめに
個人開発の手順をまとめました。
手順通り進めれば、必ずサービスは出来ます。各ステップが、なるべく省エネになるように意識して書いています。
本手順の目指すスタイルは、小さく繰り返すプラン②です
是非、参考にしてみて下さい。
( twitterやってます!@gjfcgmi )目次
STEP タイトル 内容 ① アイディアを探す まずはアイディアはどう探すのか?
実際アイディア出しに使ったエクセルを紹介② サービスを練り上げる 見切り開発を減らして、クローズ率を少しでも下げる ③ 時間を節約できる箇所? 時短ポイントを3つ紹介
④ 開発の進め方 開発着手前が大事なことを伝えたい ⑤ ユーザの獲得方法 RTされるコツとDM作戦が大事なことを伝えたい コンテンツ
① アイディアを探す
一番最初に、サービスの核となるアイディアを探します。
アイディアがなく、何から着手すれば良いか分からない時はよくあります。
3つ探し方を紹介します。①-1 美味しんぼ法
1. 他人のサービスを見る 2. いいなと思うサービスを探す 3. そのサービスを素因数分解する(どの要素がいいのか書き出す) 4. その類似サービスを探す 5. また素因数分解をする 6. 4.-5.を繰り返す。 7. 良い箇所を掛け合わせる 8. 要素を1つ変えて、サービスが成り立つか考えるイメージは、「美味しんぼ」です。
「美味しんぼ」の主人公が料理を食べた時に、具材を細かく言い当て・分析します。
そのようなイメージです?美味しんぼ法を実践した例
「インフルエンサー×広告主のC2Cサービス」を作ったときの結果です。
(一部消えていたので、参考程度ですが)①-2 他人のアイディアを見る
他人のサービスが見られるサイトを紹介します。
サイト名 紹介 Product Hunt
The best new products in tech.毎日かなりのサービスが投稿されています。
個人開発者から大手サービスまで、おそらく世界最大の投稿サービス
スマホアプリもあるので、暇な時に見るのがおすすめBetaList:
Discover and get early access to tomorrow's startupsUIが綺麗
個人開発は少なく、スタートアップが多いイメージStartup Lister 投稿数が少ないが、その分シンプルで見やすい。
まずはこのサイトを見てみるのがおすすめService Safari こちらも投稿数が少ないが、日本語サイトなので、ピックアップ。 慣れてきたら、機能がしっかりしている
Product Hunt
のみで良いと思います。①-3 友達の話は、しっかり聞く
他業種の友人が入れば、業務について根掘り葉掘り聞くようにしています。
その結果、いくつかサービスに結びつきました。
テストユーザになってくれる点も有り難いです。? Point (専門知識・業務フロー)x(IT技術)の観点を意識して話を聞くと、アイディアが浮かび易い ex: 弁護士 x 機械学習 → 類似判例検索エンジン↓のサービスは弁護士の知り合いとの話がきっかけで出来たサービスです。
新卒, Webサービスを作ってみた話他にも保育士や土地管理業者など様々な人の手を借りつつ、サービスは作っています。
② サービスを練り上げる
アイディア出しをしているうちに、いくつかサービスのイメージは出来ていると思います。
その中で、可能性の高いサービスを見つけて、さらに磨きをかけるステップです。②-1 コンセプト良し悪し占い
自身のアイディアに当てはめて、考えてみて下さい。
②-2 サービスのコンセプトを話してみる
誰かにコンセプトを話してみることをおすすめします。
きちんと言語化できるかは、早い段階で確認した方が良いです? Tips 開発終盤で、コンセプトが弱いことに気づくことは多々あります この段階で、類似サービスとしっかり違いを出せるか検討する時間を取った方が良いですこの段階で没案になる可能性は高いです。。
ダメでしたら、またアイディア探しに戻ります②-3 もっとサービスを練り上げる
収益を考えるなら、もう少しサービスを練り上げます
下記の項目をさらっと紙に書き出せるか試してみることをお勧めします。
項目 回答 サービスの全体像 解決出来ること ビジネスモデル ビジネスモデル(お金の流れ中心) 利用方法 市場の規模 収益構造 書き出している時に疑問点・類似サービスも見つかるので
STEP②には時間を書けるようにしていますビジネスコンテストではないので、ざっくりやってみるのお勧めです。
? 実例
C2Cサービスを作成した時のメモです。
(縦長になってしまうので、横で)
サービスの全体像 解決出来ること ビジネスモデル ビジネスモデル(お金の流れ中心) 利用方法 市場の規模 収益構造 ③ 時間を節約できる箇所?
③-1 デザイン
デザインは、費用対効果が高いので買う。
綺麗なコードが買えますし、何より安いです。
→「design template premium
」でぐぐるべしテンプレートが買えるサイトを一つピックアップします
WordPress Themes & Website Templates from ThemeForest自分は、こちらのサイトで買いました。
1. creative-tim
2. materializecss③-2 機能の精査
個人開発で即クローズは多々あります。
使われない機能に費やす時間は勿体無いので、実装する機能を一回精査します。
No. 問いかけ 問の意図 1 運用でカバー出来ないか サービス立ち上げ初期はそこまで人は来ません。
最初はお問い合わせベースで、件数増えてきたら機能実装でも良いはず2. 今の段階で、本当に必要か No.1と似ています。
例えば、決済画面。
会員数0で決済画面を作る必要があるのか。
段階リリースをして、手応えを感じてからでも良いはず3 ネイティブアプリである必要があるか リリース(審査)で時間がかかってしまうので、
特にネイティブである必要がなければ、webサイトで良いはず
手応え感じたら、側ネイティブを作って行く選択肢もあり4 代用出来るSaaSがないのか フルスクラッチで作成すると時間がかかるので、
出来る限り利用できそうなSaaSを見つけて、フリープランで試せないか探します
毎回使うのはこの辺り
VPSの代りにfirebase, 決済サービスStripe。firebaseはCronが設定出来るので、NoSqlで表現しづらい機能が実装できる。③-3 使うツール・サービスを絞る
個人開発では、企画・開発・マーケティングまで幅広く担当します。
学習コストもかなり膨らんでくるので、極力ツールを減らし、時短を狙うツール
ツール名 ざっくり説明 参考リンク エクセル 絶対絶対必須。
基本全部これで完結(Macであれば、Numbers)
調査からWBS,不具合管表までシートで分て1ファイルにまとめてます
↓こんな感じ- Sketch 図作成ツール。
エクセルで出来ないことは、スケッチでやっています。
記事の中の資料もスケッチで作っています学習用記事の該当箇所 IntelliJ IDEA
WebStorm超高機能なIDE
大抵の機能がデフォルトで備わっているのが、Goodですhttps://www.jetbrains.com/ja-jp/ 他のツールは使っていません。
サービス
サービス名 ざっくり説明 参考リンク Googleアナリティクス アクセス集計 アクセス解析担当が必ずやってるGoogleAnalytics設定のまとめ【2018年版】 Firebase
Herokuサーバの代わり。
ドメイン設定も簡単に出来る。SSL設定も。公式サイトががわかりやすい。
Firebase・HerokuFreeLogoDesign ロゴ自動作成ツール 他にも色々あるみたいなので。
https://liskul.com/logomaker-29137git等は割愛です
④ 開発の進め方
④-1 開発着手前
1. 思いつく機能を書き出す。 2. 必須な機能を絞る(参考: ③-2 機能の精査) 3. 機能の優先順位を決定 4. 開発順番を考える 5. スケジュールに落とし込む 1. 開発順番が早い順に機能を書き出す 2. 実装をイメージして、一段階深掘り 3. ざっくり日数見積もり 4. 見積もり結果を踏まえて、どこで初回リリースするか決める? 「5. スケジュールに落とし込む」の実例
C2Cサービスを作成した時の予定表を使います
5.1
~5.3
で出来上がる成果物を添付しました。
ステップ 5-1.
開発順番が早い順に機能を書き出す5-2.
実装をイメージして、一段階深掘り5-3.
ざっくり日数見積もり成果物 青セルは実施予定日 ④-2 開発中
予定表通り進めて、必要に応じてスケジュール変更を繰り返すのみ
開発中に出てきそうな疑問をQ&A形式で紹介します。? Tips ウェブサービスを作るなら、スマホに最適化を。 立ち上げ初期は、9割スマホからのアクセス。その後、しばらく7割程度に落ち着く (あくまでも体感)⑤ ユーザの獲得方法
SEO対策
一応導入するが、今はそこまで時間はかけない。(追々対応する。
下記のサイトを参考に、キーワードをよしなに作る。
- Google トレンド
- ラッコワードSNS投稿作戦
RT伸びやすいようにコツを紹介
1. サービス概要が分かる画像を添付 2. 読み飛ばされないように、改行は沢山使う 3. シンプルに 4. 「-----」、絵文字等でもっとシンプルに 5. 最後に何をして欲しいかを一言でDM作戦
最低限の機能の実装が終わったら、一回開発をストップ & 検証フェーズへ
DMを送って、受信者の反応を見て
クローズするか、もう少し開発継続するか決めています。意外とやっていない人多いので、DM作戦お勧めです。
返信率は、ざっくり50%~70%くらいです。
(スパムのように送ってしまうとまずいので、その辺りは考慮が必要です)終わりに
まだまだ書きたい事があります。。
後半、疲れが出てしまいました。
特に、集客パート。
開発続けるか、クローズするかのKPI表についても書きたかった今後アップデートしてきます
変更通知送るので、ストック?いいねして頂ければと思います。個人開発やっていて思うことは、楽しいです!
以上!!
- 投稿日:2020-12-01T15:32:29+09:00
Active Storageで画像のサイズを指定したら表示速度が大幅改善したよ
はじめに
問題
- ページに表示したい画像の量が多くてページの読込に時間がかかる
- ユーザーの投稿画像を表示するので事前にサイズの調整ができない
環境
- Active Storage
- mini_magick
- image_processing
- EC2 & S3 利用
考えたこと①
アップロード時にリサイズしたい
=> やり方わからず断念考えたこと②
読込時にサーバー側でリサイズしたら速くならないかな
=> できた!参考
こちらの記事を参考にしました。
Active StorageのVariantの指定方法いろいろまた日本語のリファレンスにはメソッドの細かい説明がなかったので、こちらの英語で書かれたリファレンスを参考にしました。
ActiveStorage::Variantやはり公式に一度目を通すのは大事ですね。image_processingがないと変換できないようです。なんとなくですが概要を理解できた気がします。
Google翻訳様様です。
手順
具体的な手順は参考のURLからご確認いただければと思います。
やったこと
example.rbimage_tag @hoge.image.variant(combine_options:{gravity: :center, resize:"AxB^", crop:"AxB+0+0"}).processed, class: 'hoge'variantメソッドを使って
- リサイズ
- センターフィット
- カット
を行いました。サイズはCSSで指定していたのでそれに合わせました。(こうなるとCSSのサイズ表記はいらない、しない方がいいのかな?)
また、processedオプションをつけることで2度目以降の読込はリサイズしたURLを返すことができるのでより速くなるそうです(ちょっとロジックがよくわかなくて自信ないですが…この辺はもっとしっかり勉強しないとダメですね)
結論、このようにCSSにすべて調整させるのではなくサーバーサイドで画像を整えてからフロントに渡すことが大事とのことです。
改善前
サイトトップページがこちら
- 投稿一覧にアバターを載せておりページあたり数十件単位で画像がロードされる
- 投稿にも任意で画像添付でき、一覧に表示させている
読込完了まで、長い時で10秒以上かかっている時もありました。
サイトパフォーマンスがこちら
スクショに含め忘れたのですが、読込完了まで4.3秒(たしか)というスコアでした。
あきらかに画像が足を引っ張っているのがわかります。計測について
今回はPageSpeed Insightsで読込速度を計測しました。
PageSpeed Insightsは「WEB ページの読み込み時間を短くしよう」こちらの記事で教わりました。
サイトパフォーマンスについては本当にこれが正しいのか、という指摘もあるようですが、とりあえず現時点で広く使われているサービスですし、入口としては使って問題ないのかなと思っています。改善後
読込完了まで4.0秒になりました。
点数と読込完了時間に関しては期待した程の結果は出ませんでしたが、改善項目から画像が一気に姿を消しました。
これはなかなか素晴らしい改善ではないでしょうか。大満足です。(ちなみに、PageSpeed InsightsではモバイルとPCで結果が変わるのですが、ここに載せたのはモバイルの結果です。PCでは94点(0.9秒)を記録しています。となると、レスポンシブ関係のフロント記述あるいはフロント&サーバー間の処理が悪いのかもしれません)
おわりに
本当はアップデート時にリサイズして保存したかったのですが、いったん諦めてvariantを使って表示処理を改善してみました。
これはこれで新しい発見があったのでよかったです。
あとはNginxと絡めて処理速度を上げたりできるっぽいので(アップデートの話かな?)落ち着いたら他の方法もいろいろ試していきたいと思います。
✔︎
- 投稿日:2020-12-01T15:32:29+09:00
Active Storage:画像のサイズを指定したら表示速度が大幅改善したよ
はじめに
問題
- ページに表示したい画像の量が多くてページの読込に時間がかかる
- ユーザーの投稿画像を表示するので事前にサイズの調整ができない
環境
- Active Storage
- mini_magick
- image_processing
- EC2 & S3 利用
考えたこと①
アップロード時にリサイズしたい
=> やり方わからず断念考えたこと②
読込時にサーバー側でリサイズしたら速くならないかな
=> できた!参考
こちらの記事を参考にしました。
Active StorageのVariantの指定方法いろいろ【2020.12.1追記】
改めてログを確認すると"Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command. Passing :combine_options will not be supported in Rails 6.1."と警告が…
こちらの書き方はRails 6.1 からは使えなくなるっぽいです
そして解消方法を探したところ次の記事が参考になりましたので掲載致しますまた日本語のリファレンスにはメソッドの細かい説明がなかったので、こちらの英語で書かれたリファレンスを参考にしました。
ActiveStorage::Variantやはり公式に一度目を通すのは大事ですね。image_processingがないと変換できないようです。なんとなくですが概要を理解できた気がします。
Google翻訳様様です。
手順
具体的な手順は参考のURLからご確認いただければと思います。
やったこと
example.rbimage_tag @hoge.image.variant(combine_options:{gravity: :center, resize:"AxB^", crop:"AxB+0+0"}).processed, class: 'hoge'variantメソッドを使って
- リサイズ
- センターフィット
- カット
を行いました。サイズはCSSで指定していたのでそれに合わせました。(こうなるとCSSのサイズ表記はいらない、しない方がいいのかな?)
また、processedオプションをつけることで2度目以降の読込はリサイズしたURLを返すことができるのでより速くなるそうです(ちょっとロジックがよくわからなくて自信ないですが…この辺はもっとしっかり勉強しないとダメですね)
結論、このようにCSSにすべて調整させるのではなくサーバーサイドで画像を整えてからフロントに渡すことが大事とのことです。
【2020.12.1追記】
こちらの記述だと先述のとおり Rails 6.1 からはサポートされなくなります。
combine_options がNGのようですね。
よって下記のように書き換えました。
example.rbimage_tag @hoge.image.variant(resize_to_fill: [128, 128]), class: 'hoge'やっていることは同じです。
processedオプションについてはちょっとわからなかったので、追々調べたいと思います。あまりパフォーマンスに影響していない気もしていて、とりあえず記述消してしまいました。
改善前
サイトトップページがこちら
- 投稿一覧にアバターを載せておりページあたり数十件単位で画像がロードされる
- 投稿にも任意で画像添付でき、一覧に表示させている
読込完了まで、長い時で10秒以上かかっている時もありました。
サイトパフォーマンスがこちら
スクショに含め忘れたのですが、読込完了まで4.3秒(たしか)というスコアでした。
あきらかに画像が足を引っ張っているのがわかります。計測について
今回はPageSpeed Insightsで読込速度を計測しました。
PageSpeed Insightsは「WEB ページの読み込み時間を短くしよう」こちらの記事で教わりました。
サイトパフォーマンスについては本当にこれが正しいのか、という指摘もあるようですが、とりあえず現時点で広く使われているサービスですし、入口としては使って問題ないのかなと思っています。改善後
読込完了まで4.0秒になりました。
点数と読込完了時間に関しては期待した程の結果は出ませんでしたが、改善項目から画像が一気に姿を消しました。
これはなかなか素晴らしい改善ではないでしょうか。大満足です。(ちなみに、PageSpeed InsightsではモバイルとPCで結果が変わるのですが、ここに載せたのはモバイルの結果です。PCでは94点(0.9秒)を記録しています。となると、レスポンシブ関係のフロント記述あるいはフロント&サーバー間の処理が悪いのかもしれません)
おわりに
本当はアップデート時にリサイズして保存したかったのですが、いったん諦めてvariantを使って表示処理を改善してみました。
これはこれで新しい発見があったのでよかったです。
あとはNginxと絡めて処理速度を上げたりできるっぽいので(アップデートの話かな?)落ち着いたら他の方法もいろいろ試していきたいと思います。
✔︎
- 投稿日:2020-12-01T12:07:35+09:00
Rangeパーティションを削除する方法
パーティションしたテーブルを戻したい事はほとんどないような気がしますが、
たまたまやる機会があったので備忘録的な物として残しておきます。まずは、普通に作ったパーティションを全て削除しようと思いました。
ALTER TABLE table_name DROP PARTITION p201905, p201908,
p201911, p202002, p202005, p202008, p202011, p202102, p202105, p202108, p202111;
Cannot remove all partitions, use DROP TABLE instead
というエラーが出て駄目でした。解決方法
ALTER TABLE table_name REMOVE PARTITIONING;
件数に寄りますがかなり時間が掛かります。
create unique index unique_index_activities_id on activities (id) ; ALTER TABLE table_name DROP PRIMARY KEY; ALTER TABLE table_name ADD PRIMARY KEY (id) ; DROP INDEX uniq_index_activities_id_and_created_at ON table_nameあとは、プライマリーキーを削除するために、一旦ユニークキーを作成して、プライマリーキーを削除して
IDで再度作り直して、Rangeパーティションのユニークインデックスキーを削除しました。
- 投稿日:2020-12-01T11:58:34+09:00
Devise入門64のレシピ その2
これは「フィヨルドブートキャンプ Advent Calendar 2020」の1日目の記事です。
フィヨルドブートキャンプ Part 1 Advent Calendar 2020 - Adventar
フィヨルドブートキャンプ Part 2 Advent Calendar 2020 - Adventarqiitaの文字数制限(?)に引っかかるようなので記事を分割しました。
Devise入門64のレシピ その1
Devise入門64のレシピ その2目次
- 環境
- 第1章 Deviseをはじめよう
- 第2章 モジュールを使う
- 第3章 ビューをカスタマイズする
- 第4章 コントローラーをカスタマイズする
- 第5章 モデルをカスタマイズする
- 第6章 ルーティングをカスタマイズする
- 第7章 メーラーをカスタマイズする
- 第8章 I18nをカスタマイズする
- 第9章 設定をカスタマイズする
- 第10章 その他のカスタマイズ(Wikiまとめ)
- 第11章 Tips
- 第12章 Deviseのエコシステム
- 第13章 Devise内部を知る
- 第14章 認証gemの比較
- Deviseの情報源
- チートシート
第10章 その他のカスタマイズ(Wikiまとめ)
? DeviseのWiki( https://github.com/heartcombo/devise/wiki )にはいろんなカスタマイズのやり方が書かれているよ。Wikiの中から、知っておくと役立ちそうなカスタマイズをまとめておくね。
041 パスワード変更時にログアウトさせない
? Deviseの仕様で、ログイン中にユーザーがパスワードを変更すると自動的にログアウト状態になってしまうよ。ログイン状態を維持するためには
bypass_sign_in(user)
を利用すればOKだよ。sign_in(user, :bypass => true)
を使う方法はdeprecatedなので注意してね。詳しくは以下の記事を参考にしてね。
- How To: Allow users to edit their password · heartcombo/devise Wiki · GitHub
- ruby on rails - Devise logging out automatically after password change - Stack Overflow
- Deviseでパスワード変更時にログアウトしない方法 - ワシはワシが育てる
- Deviseでパスワード変更した場合のログアウトを防ぐ | 自転車で通勤しましょ♪ブログ
042 Deviseに独自の認証方法(Strategy)を追加する
? DeviseはWardenを利用しているため、独自のStrategyクラスを定義することで、独自の認証方法を追加できるよ。詳しくは以下の記事を参考にしてね。あと057 Wardenも参考になるよ。
- How To: Authenticate via LDAP · heartcombo/devise Wiki · GitHub
- Rails/Deviseを利用した認証を Amazon Cognito 認証に委譲する - stmn tech blog
- deviseの独自ストラテジーの作り方 - Adwaysエンジニアブログ
- Deviseに独自のstrategyを入れる - あすたぴのブログ
043 ゲストユーザー機能を実装する
? ゲストユーザー機能はサインアップ無しでアプリケーションを利用できるようになる機能だよ。ユーザーは個人情報を提供せずにアプリケーションを試せるようになるよ。詳しくは以下の記事を参考にしてね。
- How To: Create a guest user · heartcombo/devise Wiki · GitHub
- #393 Guest User Record - RailsCasts
- 簡単ログイン・ゲストログイン機能の実装方法(ポートフォリオ用) - Qiita
- [Rails] devise を利用 ゲストユーザー機能( ポートフォリオ 用) - Qiita
044 アカウント削除を論理削除にする
? アカウント削除をすると、デフォルトではusersレコードを物理削除するよ。これを論理削除に変更して、usersレコードは残したままログインできないようにするよ。詳しくは以下の記事を参考にしてね。
- How to: Soft delete a user when user deletes account · heartcombo/devise Wiki · GitHub
- 【翻訳】deviseで論理削除を実装する方法 - Qiita
- [Rails]paranoiaを使わずに退会機能を実装する(ユーザーを論理削除する) - Qiita
045 管理者権限を用意する
? Deviseで管理者を用意するには、Userモデル以外にAdminモデルを用意する方法があるよね(023 複数モデルを利用する)。別のやり方として、モデルはUser1つだけにして、roleのようなカラムで管理者権限を利用する方法があるよ。Deviseが認証のgemであるのに対して、この権限の管理には認可のgemを利用するんだ。認可のgemとしては
CanCanCan
とPundit
の2つが有名だよ。この2つはできることはほとんど同じなので、どちらか好きな方を利用すればOKだよ。CanCanCanがロール起点で権限を定義するのに対して、Punditはリソース起点で権限を定義するよ。詳しくは以下の記事を参考にしてね。CanCanCan
- GitHub - CanCanCommunity/cancancan: The authorization Gem for Ruby on Rails.
- How to use CanCan / CanCanCan - Qiita
Pundit
- GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes
- Pundit + Railsで認可の仕組みをシンプルに作る - Qiita
046 emailとusernameどちらでもログインできるようにする
? 通常だとemailでログインするけど、これがemailとusernameどちらでもログインできるようになったら嬉しいよね。Userモデルにemail属性とusername属性の2役をこなす
login
という仮想属性を用意すれば実現できるよ。詳しくは以下の記事を参考にしてね。
- How To: Allow users to sign in using their username or email address · heartcombo/devise Wiki · GitHub
- Rails deviseによるユーザー認証 メールによる認証、emailとusernameのどちらでもログイン可能にするまで - Qiita
- Rails4.1でdeviseのログインIDをemailとusernameのどちらでも可能にする - Qiita
047 パスワード入力なしでアカウント情報を変更する
? アカウント編集画面(
/users/edit
)で自分のアカウント情報を変更するためには、現在のパスワードの入力が必須だよ。これをパスワードなしで変更できるようにするよ。詳しくは以下の記事を参考にしてね。
- How To: Allow users to edit their account without providing a password · heartcombo/devise Wiki · GitHub
- Devise でユーザーがパスワードなしでアカウント情報を変更するのを許可 | EasyRamble
048 パスワードをbcrypt以外の方法でハッシュ化する
? Deviseではデフォルトでbcryptを使いパスワードをハッシュ化するよ。
devise-encryptable
gemを使うことで別の方法でハッシュ化できるようになるよ。詳しくは以下の記事を参考にしてね。
- How To: Create a custom encryptor · heartcombo/devise Wiki · GitHub
- GitHub - heartcombo/devise-encryptable: Devise encryptable behavior since v2.1
- deviseの暗号化アルゴリズムの変更方法 - aik's blog
049 メールアドレス変更時にもConfirm指示メールを送信する
? Confirmableモジュールはデフォルトではメールアドレス変更時にはConfirm指示メールを送信しないよ。これを修正するには
config.reconfirmable = true
という設定をする必要があるよ。詳しくは以下の記事を参考にしてね。
第11章 Tips
050 userをserializeする
?? RailsコンソールでUserインスタンスを見ると
encrypted_password
などの属性が表示されないよ?なんで?irb(main):009:0> User.first => #<User id: 2, email: "shita@example.com", created_at: "2020-11-06 06:06:36", updated_at: "2020-11-06 06:06:36">? Deviseではセキュリティー上の都合で、必要なカラムだけをシリアライズするようになってるんだ。usersテーブルに
encrypted_password
(ハッシュ化されたパスワード)やcurrent_sign_in_ip
(サインイン時のIPアドレス)カラムなどのセンシティブな情報を持たせることになるでしょ?Userインスタンスを丸ごとシリアライズしてしまうと、場合によってはそれらの情報が漏れてしまう可能性があるんだ。だからDeviseではserializable_hash
をオーバーライドしてセンシティブな情報はシリアライズされないようにしているんだよ。RailsコンソールではUserインスタンスの状態がinspect
を使って表示されるけど、inspect
もserializable_hash
を利用するようにオーバーライドされているため、Railsコンソールではencrypted_password
などのカラム情報が表示されないようになっているよ。irb(main):016:0> User.first.serializable_hash => {"id"=>2, "email"=>"shita@example.com", "created_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00, "updated_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00}? 具体的には以下の属性はシリアライズされないよ。
encrypted_password reset_password_token reset_password_sent_at remember_created_at sign_in_count current_sign_in_at last_sign_in_at current_sign_in_ip last_sign_in_ip password_salt confirmation_token confirmed_at confirmation_sent_at remember_token unconfirmed_email failed_attempts unlock_token locked_at?
serializable_hash(force_except: true)
を使ったりattributes
を使えばencrypted_password
fなどの情報にもアクセスできるよ。irb(main):017:0> User.first.serializable_hash(force_except: true) => {"id"=>2, "email"=>"shita@example.com", "encrypted_password"=>"$2a$12$9Fiz99wL33TIw8JeDP2Vb..y99m5i0JrMY8pjeekmumXNOwM1ncbS", "reset_password_token"=>nil, "reset_password_sent_at"=>nil, "remember_created_at"=>nil, "created_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00, "updated_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00}irb(main):018:0> User.first.attributes => {"id"=>2, "email"=>"shita@example.com", "encrypted_password"=>"$2a$12$9Fiz99wL33TIw8JeDP2Vb..y99m5i0JrMY8pjeekmumXNOwM1ncbS", "reset_password_token"=>nil, "reset_password_sent_at"=>nil, "remember_created_at"=>nil, "created_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00, "updated_at"=>Fri, 06 Nov 2020 06:06:36 UTC +00:00}参考
- devise/authenticatable.rb at master · heartcombo/devise · GitHub
- パーフェクトRails著者が解説するdeviseの現代的なユーザー認証のモデル構成について - joker1007’s diary
051 モデルからcurrent_userにアクセスする
?? モデルからcurrent_userにアクセスしたいのだけど、どうすればいい?
?
ActiveSupport::CurrentAttributes
を利用すれば可能だよ。ActiveSupport::CurrentAttributes
を継承したクラスはリクエスト毎に属性がリセットされるため、リクエスト毎に独立した状態を持てるようになるんだ。? まず
ActiveSupport::CurrentAttributes
を継承したCurrent
クラスを定義するよ。class Current < ActiveSupport::CurrentAttributes # この属性がcurrent_userになる # この属性はリクエスト毎にリセットされる attribute :user end? application_controller.rbの
before_action
でCurrent.user
にcurrent_user
をセットするよ。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :set_current_user def set_current_user Current.user = current_user end end? これでモデルから
current_uesr
にアクセスできるようになるよ。モデルclass Article < ApplicationRecord scope :current, -> { where(user: Current.user) } endArticle.current #=> current_userのarticles? ただし
ActiveSupport::CurrentAttributes
の利用は基本的にはアンチパターンだと思うので、できれば避けるべきかな〜と思うよ。ActiveSupport::CurrentAttributes
はグローバル変数のようなものでどこからでもアクセスできてしまうから、MVCが壊れてコードがカオスになっちゃうんだ。あとRailsコンソールでの利用とか、リクエストがない状態だとエラーになっちゃうしね。モデルでcurrent_user
が必要になる場合は、current_userを引数として渡すか、current_user.articles
のように関連を使うかしたほうがいいよ。参考
052 テスト
? Deviseの機能をテストで使うにはヘルパーモジュールをincludeすればOKだよ。
# コントローラーテストの場合 class PostsControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers end # Integrationテストの場合 class PostsTests < ActionDispatch::IntegrationTest include Devise::Test::IntegrationHelpers end? RSpecの場合は設定ファイルでincludeしてね。
RSpec.configure do |config| # コントローラーテストの場合 config.include Devise::Test::ControllerHelpers, type: :controller # Integrationテストの場合 config.include Devise::Test::IntegrationHelpers, type: :request end? これでテストでヘルパーメソッドとして
sign_in
とsign_out
が使えるようになるよ。テスト# ログイン sign_in user # ログイン(スコープ指定) sign_in user, scope: :admin # ログアウト sign_out user # ログアウト(Scope指定) sign_out :admin第12章 Deviseのエコシステム
? DeviseはRailsの認証gemの中では一番人気があるので、Devise関係の便利なgemがたくさん存在するよ。ここではそんなgemの中でもとりわけ便利なgemを紹介していくよ。
053 AnyLogin - ログインユーザーを切り替える
? 開発環境でログインユーザーを切り替えるのって面倒だよね?いちいちログインし直さなきゃいけなかったり、ユーザーのパスワードを覚えておかなきゃいけなかったり。AnyLoginを使うとログインユーザーをドロップダウンから選択できるようになるよ。
AnyLoginを使ってみよう
? まずはAnyLoginをインストールしてね。
Gemfilegem 'any_login'$ bundle install?
application.html.erb
に以下のコードを追加してね。app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>DemoApp</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> <!-- この行を追加 --> <%= any_login_here if defined?(AnyLogin) %> </body> </html>? これで画面の左下からログインユーザーを選択できるようになるよ。
設定を変更する
? ジェネレータを使って設定ファイルを作成してね。
$ rails g any_login initializer create config/initializers/any_login.rb? 設定ファイルの設定項目はこんな感じだよ。
config/initializers/any_login.rbAnyLogin.setup do |config| # provider (:devise, :authlogic, sorcery, clearance)を指定する。Gemfileから自動的に判定されるのでnilのままでOKだよ。 config.provider = nil # 有効にするかどうか。基本的には'development'だけ有効にすればOKだよ。 config.enabled = Rails.env.to_s == 'development' # 認証対象のモデルクラス名。たいていはUserでOK config.klass_name = 'User' # Userのコレクションを返すメソッドを指定。デフォはUser.all config.collection_method = :all # セレクトボックスのフォーマット config.name_method = proc { |e| [e.email, e.id] } # ログインユーザー選択後のリダイレクト先 config.redirect_path_after_login = :root_path # ログイン発火イベント(change, click, both) config.login_on = :both # 表示位置(top_left, top_right, bottom_left, bottom_right) config.position = :bottom_left # ログインボタンのラベル config.login_button_label = 'Login' # セレクトボックスのプロンプト config.select_prompt = "Select #{AnyLogin.klass_name}" # デフォルトでログインユーザー選択formを展開する config.auto_show = false # limit(数値 or :none) config.limit = 10 # ベーシック認証ON config.http_basic_authentication_enabled = false # ベーシック認証(ユーザー名) config.http_basic_authentication_user_name = 'any_login' # ベーシック認証(パスワード) config.http_basic_authentication_password = 'password' # controllerを使った表示ON/OFF条件 config.verify_access_proc = proc { |controller| true } end参考
054 devise-i18n - ビューを日本語化する
? devise-i18nというgemを使うとビューを日本語化できるよ。詳しくは039 ビューを日本語化するを参照してね。
055 DeviseInvitable - 招待機能を追加する
DeviseInvitableを使ってみよう
? DeviseInvitableは招待機能をDeviseに追加するgemだよ。Invitableモジュールを利用することで、指定されたメールアドレスに招待状を送信できるようになるよ。
? まずはDeviseInvitableをインストールするよ。
Gemfilegem 'devise_invitable'$ bundle install? ジェネレーターを実行して、必要なファイルを作成してね。
$ rails g devise_invitable:install insert config/initializers/devise.rb create config/locales/devise_invitable.en.yml?
config/initializers/devise.rb
にDeviseInvitable用の設定が追加されるよ。config/initializers/devise.rb# 追加部分のみ # ==> Configuration for :invitable # The period the generated invitation token is valid. # After this period, the invited resource won't be able to accept the invitation. # When invite_for is 0 (the default), the invitation won't expire. # config.invite_for = 2.weeks # Number of invitations users can send. # - If invitation_limit is nil, there is no limit for invitations, users can # send unlimited invitations, invitation_limit column is not used. # - If invitation_limit is 0, users can't send invitations by default. # - If invitation_limit n > 0, users can send n invitations. # You can change invitation_limit column for some users so they can send more # or less invitations, even with global invitation_limit = 0 # Default: nil # config.invitation_limit = 5 # The key to be used to check existing users when sending an invitation # and the regexp used to test it when validate_on_invite is not set. # config.invite_key = { email: /\A[^@]+@[^@]+\z/ } # config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil } # Ensure that invited record is valid. # The invitation won't be sent if this check fails. # Default: false # config.validate_on_invite = true # Resend invitation if user with invited status is invited again # Default: true # config.resend_invitation = false # The class name of the inviting model. If this is nil, # the #invited_by association is declared to be polymorphic. # Default: nil # config.invited_by_class_name = 'User' # The foreign key to the inviting model (if invited_by_class_name is set) # Default: :invited_by_id # config.invited_by_foreign_key = :invited_by_id # The column name used for counter_cache column. If this is nil, # the #invited_by association is declared without counter_cache. # Default: nil # config.invited_by_counter_cache = :invitations_count # Auto-login after the user accepts the invite. If this is false, # the user will need to manually log in after accepting the invite. # Default: true # config.allow_insecure_sign_in_after_accept = false?
devise_invitable.en.yml
というDeviseInvitable用のロケールファイルが作成されるよ。config/locales/devise_invitable.en.ymlen: devise: failure: invited: "You have a pending invitation, accept it to finish creating your account." invitations: send_instructions: "An invitation email has been sent to %{email}." invitation_token_invalid: "The invitation token provided is not valid!" updated: "Your password was set successfully. You are now signed in." updated_not_active: "Your password was set successfully." no_invitations_remaining: "No invitations remaining" invitation_removed: "Your invitation was removed." new: header: "Send invitation" submit_button: "Send an invitation" edit: header: "Set your password" submit_button: "Set my password" mailer: invitation_instructions: subject: "Invitation instructions" hello: "Hello %{email}" someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below." accept: "Accept invitation" accept_until: "This invitation will be due in %{due_date}." ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password." time: formats: devise: mailer: invitation_instructions: accept_until_format: "%B %d, %Y %I:%M %p"? ジェネレーターを使ってInvitableモジュールで必要となるコードを追加するよ。Userモデルに
:invitable
が追加されて、Invitableモジュール用のマイグレーションファイルが作成されるよ。$ rails g devise_invitable User insert app/models/user.rb invoke active_record create db/migrate/20201110133651_devise_invitable_add_to_users.rbapp/models/user.rbclass User < ApplicationRecord # invitableモジュールが追加されてるよ devise :invitable, :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable enddb/migrate/20201110133651_devise_invitable_add_to_users.rbclass DeviseInvitableAddToUsers < ActiveRecord::Migration[6.0] def up change_table :users do |t| t.string :invitation_token t.datetime :invitation_created_at t.datetime :invitation_sent_at t.datetime :invitation_accepted_at t.integer :invitation_limit t.references :invited_by, polymorphic: true t.integer :invitations_count, default: 0 t.index :invitations_count t.index :invitation_token, unique: true # for invitable t.index :invited_by_id end end def down change_table :users do |t| t.remove_references :invited_by, polymorphic: true t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at end end end? マイグレーションを実行してね。
$ rails db:migrate? ビューに招待メール送信画面(
/users/invitation/new
)へのリンクを置いてね。<%= link_to "招待メール送信画面", new_user_invitation_path %>? これで招待機能が使えるようになったよ。
? 試しにリンクを踏んでみてね。リンクを踏むと招待メール送信画面(
/users/invitation/new
)に遷移するよ。? ここでメールアドレスを入力してsubmitすると、入力されたメールアドレスに招待メールが送信されるよ。
? ちなみにこの時点で招待されたユーザーのusersレコードは作成されているよ。まだ正式なユーザー登録はされていないのでログインはできないけどね。
? メールを受け取った人(招待された人)はメール内の『Accept invitation』リンクを踏むと、accept画面(
/users/invitation/accept
)に遷移するよ。? パスワードを入力してsubmitすると、正式なユーザー登録になるよ。
Invitableモジュールのコントローラーとルーティング
? Invitableモジュールでは
Devise::InvitationsController
というコントローラーが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/invitation/accept devise/invitations#edit accept画面(パスワード設定画面) GET /users/invitation/new devise/invitations#new 招待メール送信画面 PATCH/PUT /users/invitation devise/invitations#update accept POST /users/invitation devise/invitations#create 招待メール送信 GET /users/invitation/remove devise/invitations#destroy 招待取り消し Invitableモジュールのメソッド
? Invitableモジュールを使うと、いくつか便利なメソッドがUserに追加されるよ。招待状の送信は通常であれば用意されたコントローラーから行うので、これらのメソッドを直接使うことは少ないけど、手動で操作したい場合にはこれらのメソッドを使うことになるよ。
# 招待状を送る # ユーザーを作成し、招待メールを送信する # この場合はnew_user@example.comにメールを送信する User.invite!(email: 'new_user@example.com', name: 'John Doe') # ユーザーを作成し、招待メールは送信しない(ブロックで指定) User.invite!(email: 'new_user@example.com', name: 'John Doe') do |u| u.skip_invitation = true end # ユーザーを作成し、招待メールは送信しない(オプションで指定) User.invite!(email: 'new_user@example.com', name: 'John Doe', skip_invitation: true) # ユーザー作成後に招待メールを送信する # current_userはinvited_by User.find(42).invite!(current_user) # invitation_tokenで検索する User.find_by_invitation_token(params[:invitation_token], true) # invitation_tokenを使い、招待を受け入れる User.accept_invitation!(invitation_token: params[:invitation_token], password: 'ad97nwj3o2', name: 'John Doe') # invitation_accepted_atがnilのUser User.invitation_not_accepted # invitation_accepted_atがnilでないUser User.invitation_accepted # 招待で作られたユーザー User.created_by_inviteInvitableモジュールの設定
? 設定を変更したい場合は設定ファイルで変更してね。
config/initializers/devise.rbconfig.invite_for = 2.weeks? あるいはUserモデルのオプションとして指定することもできるよ。
app/models/user.rbdevise :database_authenticatable, :confirmable, :invitable, invite_for: 2.weeks? 設定項目はこんな感じだよ。
config/initializers/devise.rb# invitation_tokenの有効期限 # 有効期限を過ぎた場合は招待が無効になる # 0に設定すると有効期限なしになる # デフォルト: 0 # config.invite_for = 2.weeks # ユーザーが送信できる招待メールの上限数 # nilの場合は無制限になり、invitation_limitは利用されない # 0の場合は送信できなくなるが、手動でカラムの値を変更すれば送信可能 # デフォルト: nil # config.invitation_limit = 5 # 招待メールを送信する際に既存ユーザーをチェックするためのキー # デフォルト: emailに対してDevise.email_regexpでチェックする # config.invite_key = { email: /\A[^@]+@[^@]+\z/ } # config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil } # 招待ユーザーを強制的にvalidにする # デフォルト: false # config.validate_on_invite = true # 招待済みのユーザーが再びinvited状態になった場合に、招待メールを再送信する # デフォルト: true # config.resend_invitation = false # 招待するモデルのクラス名 # nilの場合はポリモーフィック関連が使われる # デフォルト: nil # config.invited_by_class_name = 'User' # 招待するモデルへの外部キー # デフォルト: :invited_by_id # config.invited_by_foreign_key = :invited_by_id # カウンターキャッシュのカラム名 # デフォルト: nil # config.invited_by_counter_cache = :invitations_count # 招待後自動的にログイン状態になる # デフォルト: true # config.allow_insecure_sign_in_after_accept = falseビューをカスタマイズする
? 全てのビューはDeviseInvitable gem内にパッケージ化されているよ。ビューをカスタマイズする場合は、ジェネレーターを利用してgem内のビューをアプリ内にコピーしてね。
$ rails g devise_invitable:views invoke DeviseInvitable::Generators::MailerViewsGenerator exist app/views/devise/mailer create app/views/devise/mailer/invitation_instructions.html.erb create app/views/devise/mailer/invitation_instructions.text.erb invoke form_for create app/views/devise/invitations create app/views/devise/invitations/edit.html.erb create app/views/devise/invitations/new.html.erb? ちなみに複数モデルを利用する場合はScopeを指定することも可能だよ。
$ rails g devise_invitable:views users create app/views/users/mailer create app/views/users/mailer/invitation_instructions.html.erb create app/views/users/mailer/invitation_instructions.text.erb invoke form_for create app/views/users/invitations create app/views/users/invitations/edit.html.erb create app/views/users/invitations/new.html.erbコントローラーをカスタマイズする
? コントローラーはDeviseInvitable gem内にパッケージ化されているよ。
Devise::InvitationsController
というコントローラーだよ。# https://github.com/scambra/devise_invitable/blob/master/app/controllers/devise/invitations_controller.rb class Devise::InvitationsController < DeviseController prepend_before_action :authenticate_inviter!, only: [:new, :create] prepend_before_action :has_invitations_left?, only: [:create] prepend_before_action :require_no_authentication, only: [:edit, :update, :destroy] prepend_before_action :resource_from_invitation_token, only: [:edit, :destroy] if respond_to? :helper_method helper_method :after_sign_in_path_for end # GET /resource/invitation/new def new self.resource = resource_class.new render :new end # POST /resource/invitation def create self.resource = invite_resource resource_invited = resource.errors.empty? yield resource if block_given? if resource_invited if is_flashing_format? && self.resource.invitation_sent_at set_flash_message :notice, :send_instructions, email: self.resource.email end if self.method(:after_invite_path_for).arity == 1 respond_with resource, location: after_invite_path_for(current_inviter) else respond_with resource, location: after_invite_path_for(current_inviter, resource) end else respond_with_navigational(resource) { render :new } end end # GET /resource/invitation/accept?invitation_token=abcdef def edit set_minimum_password_length resource.invitation_token = params[:invitation_token] render :edit end # PUT /resource/invitation def update raw_invitation_token = update_resource_params[:invitation_token] self.resource = accept_resource invitation_accepted = resource.errors.empty? yield resource if block_given? if invitation_accepted if resource.class.allow_insecure_sign_in_after_accept flash_message = resource.active_for_authentication? ? :updated : :updated_not_active set_flash_message :notice, flash_message if is_flashing_format? resource.after_database_authentication sign_in(resource_name, resource) respond_with resource, location: after_accept_path_for(resource) else set_flash_message :notice, :updated_not_active if is_flashing_format? respond_with resource, location: new_session_path(resource_name) end else resource.invitation_token = raw_invitation_token respond_with_navigational(resource) { render :edit } end end # GET /resource/invitation/remove?invitation_token=abcdef def destroy resource.destroy set_flash_message :notice, :invitation_removed if is_flashing_format? redirect_to after_sign_out_path_for(resource_name) end protected def invite_resource(&block) resource_class.invite!(invite_params, current_inviter, &block) end def accept_resource resource_class.accept_invitation!(update_resource_params) end def current_inviter authenticate_inviter! end def has_invitations_left? unless current_inviter.nil? || current_inviter.has_invitations_left? self.resource = resource_class.new set_flash_message :alert, :no_invitations_remaining if is_flashing_format? respond_with_navigational(resource) { render :new } end end def resource_from_invitation_token unless params[:invitation_token] && self.resource = resource_class.find_by_invitation_token(params[:invitation_token], true) set_flash_message(:alert, :invitation_token_invalid) if is_flashing_format? redirect_to after_sign_out_path_for(resource_name) end end def invite_params devise_parameter_sanitizer.sanitize(:invite) end def update_resource_params devise_parameter_sanitizer.sanitize(:accept_invitation) end def translation_scope 'devise.invitations' end end? Deviseと違いコントローラーのgeneratorは存在しないので、カスタマイズする際は自分で
Devise::InvitationsController
を継承するコントローラーを作成してね。class Users::InvitationsController < Devise::InvitationsController def update # カスタマイズ end end? 自前のコントローラーを利用する場合は、ルーティングも変更する必要があるよ。
config/routes.rb# invitationsコントローラーにはusers/invitationsを使う devise_for :users, controllers: { invitations: 'users/invitations' }? あとはDeviseと同じように、
Devise::InvitationsController
のコードを見ながら自由にカスタマイズしてね。Strong Parameterをカスタマイズする
? ビューをカスタマイズする際にフォームにinput要素を追加したい場合があるよね。でもDeviseInvitableではStrong Parameterで許可される属性がデフォルトで決まっているため、ビューだけでなくStrong Parameterも変更する必要があるんだ。
? デフォルトで許可されている属性は以下の通りだよ。
コントローラー#アクション 識別子 概要 許可されている属性 devise/invitations#create :invite 招待メール送信 devise/invitations#update :accept_invitation accept invitation_token, password, password_confirmation ? 許可する属性を追加したい場合は
devise_parameter_sanitizer.permit
を使ってね。application_controller.rbbefore_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:accept_invitation, keys: [:first_name, :last_name, :phone]) end? より詳しく知りたい場合はDeviseの 021 Strong Parameterをカスタマイズする を参照してね。
I18nをカスタマイズする
? InvitableモジュールはflashメッセージでI18nを利用しているよ。そのため日本語のロケールファイルを用意することでflashメッセージを日本語化できるよ。
? 日本語のロケールファイルは Japanese locale file for DeviseInvitable · GitHub にあるよ。このファイルをダウンロードして
locales/locales/devise_invitable.ja.yml
においてね。locales/locales/devise_invitable.ja.ymlja: devise: failure: invited: 'アカウントを作成するには、保留中の招待を承認してください。' invitations: send_instructions: '招待メールが%{email}に送信されました。' invitation_token_invalid: '招待コードが不正です。' updated: 'パスワードが設定されました。お使いのアカウントでログインできます。' updated_not_active: 'パスワードが設定されました。' no_invitations_remaining: 'これ以上招待できません。' invitation_removed: '招待を取り消しました。' new: header: '招待する' submit_button: '招待メールを送る' edit: header: 'パスワードを設定する' submit_button: 'パスワードを設定する' mailer: invitation_instructions: subject: '招待を承認するには' hello: 'こんにちは、%{email}さん' someone_invited_you: '%{url}に招待されました。以下のリンクから承認できます。' accept: 'Accept invitation' accept: '招待を承認する' accept_until: 'この招待は%{due_date}まで有効です。' ignore: '招待を承認しない場合は、このメールを無視してください。<br />あなたのアカウントは上記のリンク先にアクセスしパスワードを設定するまでは作成されません。' time: formats: devise: mailer: invitation_instructions: accept_until_format: '%Y年%m月%d日%H時%M分'参考
056 Devise Security - エンタープライズなセキュリティー機能を追加する
? Devise SecurityはDeviseにエンタープライズなセキュリティー機能を追加するよ。秘密の質問だったり、パスワードに有効期限を設けたり、標準のDeviseではまかなえないセキュリティー要件にも対応できるようになるよ。
? ちなみにこのgemの元になったDevise Security Extension(devise_security_extension)というgemはもうメンテナンスされていないので、こちらのDevise Securityを利用してね。
7つのモジュール
? Devise Securityは追加のセキュリティー機能を以下の7つのモジュールとして提供するよ。
モジュール 概要 :password_expirable 一定期間経過するとパスワードが期限切れになり、ユーザーは再度パスワードを設定しないとログインできなくなる。 :password_archivable パスワード履歴を保存して、同じパスワードを使うようにする。
パスワード履歴はold_passwords
テーブルに保存する。:password_expirable
との併用が推奨されている。:security_questionable 秘密の質問機能。 :secure_validatable email/passwordに対して、Validatableモジュールより強力なバリデーションを提供する。 :expirable 指定期間非アクティブ状態が続くと、ユーザーアカウントを期限切れにする。 :session_limitable 多重ログイン禁止。
1アカウントで1セッションしか利用できなくなる。:paranoid_verification 識別コードの発行機能。 Devise Securityを使ってみよう
? 実際に使ってみよう。今回はPassword Expirableモジュールを利用して、一定期間経過するとパスワードが期限切れになるようにするよ。
? まずはgemをinstallするよ。
Gemfilegem 'devise-security'$ bundle install? ジェネレーターを実行して、設定ファイルとロケールファイルを作成するよ。
$ rails g devise_security:install create config/initializers/devise-security.rb create config/locales/devise.security_extension.en.yml create config/locales/devise.security_extension.es.yml create config/locales/devise.security_extension.de.yml create config/locales/devise.security_extension.fr.yml create config/locales/devise.security_extension.it.yml create config/locales/devise.security_extension.ja.yml create config/locales/devise.security_extension.tr.yml? 設定ファイルはこんな感じだよ。
config/initializers/devise-security.rb# frozen_string_literal: true Devise.setup do |config| # ==> Security Extension # Configure security extension for devise # Should the password expire (e.g 3.months) # config.expire_password_after = false # Need 1 char of A-Z, a-z and 0-9 # config.password_complexity = { digit: 1, lower: 1, symbol: 1, upper: 1 } # How many passwords to keep in archive # config.password_archiving_count = 5 # Deny old passwords (true, false, number_of_old_passwords_to_check) # Examples: # config.deny_old_passwords = false # allow old passwords # config.deny_old_passwords = true # will deny all the old passwords # config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords # config.deny_old_passwords = true # enable email validation for :secure_validatable. (true, false, validation_options) # dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation # config.email_validation = true # captcha integration for recover form # config.captcha_for_recover = true # captcha integration for sign up form # config.captcha_for_sign_up = true # captcha integration for sign in form # config.captcha_for_sign_in = true # captcha integration for unlock form # config.captcha_for_unlock = true # captcha integration for confirmation form # config.captcha_for_confirmation = true # Time period for account expiry from last_activity_at # config.expire_after = 90.days end? 有効にしたいモジュールを
devise
メソッドで指定するよ。今回はPassword Expirableモジュールを有効にするよ。app/models/user.rbclass User < ApplicationRecord # :password_expirableを追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :password_expirable end? Password Expirableモジュールで必要になるカラムを追加するよ。
マイグレーションファイルclass AddPasswordExpirableColumnsToUsers < ActiveRecord::Migration[6.0] def change change_table :users do |t| t.datetime :password_changed_at end add_index :users, :password_changed_at end end$ rails db:migrate? パスワードの有効期限を設定するよ。今回は動作確認のために10秒に設定するね。
config/initializers/devise-security.rb- # config.expire_password_after = false + config.expire_password_after = 10.seconds? これで完了だよ。設定を反映させるためにサーバーを再起動してね。
? 10秒待ってからログインしてみてね。するとログインエラーになって、こんな感じの『パスワードが期限切れになりました。パスワードを更新してください。』的なメッセージが表示されるよ。
? ユーザーはこの画面で新しいパスワードを設定するまでログインできなくなるよ。
? これで一定期間経過でパスワードが無効になることを確認できたね。
参考
- GitHub - devise-security/devise-security: A security extension for devise, meeting industrial standard security demands for web applications.
- devise-securityを使って、よりセキュアなRailsアプリを構築する - Qiita
第13章 Devise内部を知る
057 Warden
? WardenはRackミドルウェアを介して認証を行う仕組みを提供するgemだよ。Wardenを使うとMountable Engineみたいな他のRackアプリから認証にアクセスできたり、コントローラー層だけでなくルーティング層からも認証できたりするんだ。
? Deviseは内部的にWardenを利用しているよ。例えばDeviseにはUserとかAdminとか複数のモデルを使うためのScopeという機能があるでしょ?実はこれ、Wardenが提供する機能なんだよね。なのでWardenを理解することで、Deviseに対する理解がさらに深まるよ。
Wardenを使ってみよう
? まずはDeviseを使わずにWardenを使い認証機能を実装することで、Wardenがどんなふうに使われるかを見ていこう。
? まずはWardenをインストールするよ。
gem 'warden'$ bundle install? Strategyクラスを定義するよ。Strategyは認証のロジックを置く場所で、ここに実際の認証を行うためのコードを書くよ。今回はemailとpasswordでの認証を実装するよ。(WardenのStrategyはOmniauthのStrategyとは別物なので注意)
lib/strategies/password_strategy.rbclass PasswordStrategy < ::Warden::Strategies::Base # 実際の認証を行うロジックはここに定義する # 認証成功時はsuccess!を、認証失敗時はfail!を呼び出す def authenticate! # Strategyではparamsにアクセスできる user = User.find_by_email(params['email']) if user && user.authenticate(params['password']) # 認証成功 # 引数のuserにはコントローラーからenv['warden'].userでアクセスできる(これがDeviseでいうcurrent_userになる) success! user else # 認証失敗 # 引数のメッセージにはコントローラーからenv['warden'].messageでアクセスできる fail! "Invalid email or password" end end end # PasswordStrategyクラスをwardenのstrategyに追加する Warden::Strategies.add(:password, PasswordStrategy)? WardenをRackミドルウェアに追加するよ。
config/application.rbconfig.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager| # 先程定義したStrategyをデフォルトのStrategyとする manager.default_strategies :password end? これでコントローラーからwardenにアクセスできるようになるよ。
# request.envを介してwardenを取得できる warden = request.env['warden'] # Strategyを実行して認証する warden.authenticate! # 認証済みならtrue warden.authenticated? #=> true # current_userを取得 warden.user #=> user # ログアウトする warden.logout? Wardenは一度認証が成功すると、sessionにユーザー情報を保存してくれるよ。そのため再度認証する際には、Strategyを呼び出す前にsessionに既にユーザーが保存されているかどうかを確認して、ユーザーがあればそちらを利用してくれるよ。
? コントローラーに限らずRack環境ならどこでも
env['warden']
でwardenにアクセスできるというのがポイントだよ。参考
Strategyとは?
? Strategyは実際の認証ロジックを置く場所だよ。こんな感じで定義するよ。
lib/strategies/password_strategy.rbclass PasswordStrategy < ::Warden::Strategies::Base # 具体的な認証ロジックを定義する # 認証成功時はsuccess!を、認証失敗時はfail!を呼び出す def authenticate! # Strategyではparamsにアクセスできる user = User.find_by_email(params['email']) if user && user.authenticate(params['password']) # 認証成功 # 引数のuserにはコントローラーからenv['warden'].userでアクセスできる(これがcurrent_userになる) success! user else # 認証失敗 # 引数のメッセージにはコントローラーからenv['warden'].messageでアクセスできる fail! "Invalid email or password" end end end? Strategyでは以下のメソッドが利用できるよ。
メソッド 概要 success! 認証成功。引数にはuserを渡す fail! 認証失敗。引数にはエラーメッセージを渡す halt! 後続のStrategyを実行せずに、ここで認証処理を停止する redirect! 別のURLにリダイレクトして、認証処理を停止する ?
authenticate!
以外にvalid?
というメソッドを定義することもできるよ。valid?
はガードとしての役割を持っていて、trueを返す場合だけauthenticate!
が実行されるよ。lib/strategies/password_strategy.rbclass PasswordStrategy < ::Warden::Strategies::Base # ガードとしての役割 # trueを返す場合だけ`authenticate!`が実行される # 何も定義しないとtrueになり、常に実行される def valid? params['email'] || params['password'] end def authenticate! user = User.find_by_email(params['email']) if user && user.authenticate(params['password']) success! user else fail "Invalid email or password" end end end? Strategyは複数定義することができて、順番に実行していくことが可能だよ。その中の1つでも成功するか、全ての戦略を通るか、戦略がfail!するまで呼ばれるよ。
# PasswordStorategyとBasicStrategyを順に実行する env['warden'].authenticate(:password, :basic)? 以上がStrategyの説明になるよ。それじゃあDeviseにどんなStrategyが存在するか見ていくね。
? Deviseには2つのStrategyが存在するよ。
クラス名 概要 Devise::Strategies::DatabaseAuthenticatable emailとpasswordで認証。
Database Authenticatableモジュールで利用。Devise::Strategies::Rememberable cookieに保存したtokenで認証。
Rememberableモジュールで利用。?
Devise::Strategies::DatabaseAuthenticatable
はこんな感じだよ。コードを読むとpassword
で認証してることがわかるよ。lib/devise/strategies/database_authenticatable.rb# frozen_string_literal: true require 'devise/strategies/authenticatable' module Devise module Strategies # Default strategy for signing in a user, based on their email and password in the database. class DatabaseAuthenticatable < Authenticatable def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end # In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key. # This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't # exist in the database if the password hashing algorithm is not called. mapping.to.new.password = password if !hashed && Devise.paranoid unless resource Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database) end end end end end Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)?
Devise::Strategies::Rememberable
はこんな感じだよ。Cookieのremember_token
からuser
をデシリアライズして認証してるね。Cookieにremember_token
が存在する場合だけ認証が実行されるよ。lib/devise/strategies/rememberable.rb# frozen_string_literal: true require 'devise/strategies/authenticatable' module Devise module Strategies # Remember the user through the remember token. This strategy is responsible # to verify whether there is a cookie with the remember token, and to # recreate the user from this cookie if it exists. Must be called *before* # authenticatable. class Rememberable < Authenticatable # A valid strategy for rememberable needs a remember token in the cookies. def valid? @remember_cookie = nil remember_cookie.present? end # To authenticate a user we deserialize the cookie and attempt finding # the record in the database. If the attempt fails, we pass to another # strategy handle the authentication. def authenticate! resource = mapping.to.serialize_from_cookie(*remember_cookie) unless resource cookies.delete(remember_key) return pass end if validate(resource) remember_me(resource) if extend_remember_me?(resource) resource.after_remembered success!(resource) end end # No need to clean up the CSRF when using rememberable. # In fact, cleaning it up here would be a bug because # rememberable is triggered on GET requests which means # we would render a page on first access with all csrf # tokens expired. def clean_up_csrf? false end private def extend_remember_me?(resource) resource.respond_to?(:extend_remember_period) && resource.extend_remember_period end def remember_me? true end def remember_key mapping.to.rememberable_options.fetch(:key, "remember_#{scope}_token") end def remember_cookie @remember_cookie ||= cookies.signed[remember_key] end end end end Warden::Strategies.add(:rememberable, Devise::Strategies::Rememberable)? この2つのStrategyは同時に使うことができるよ。RememberableモジュールとDatabase Authenticatableモジュールが有効な場合、まず
Devise::Strategies::Rememberable
の認証が実行されて、次にDevise::Strategies::DatabaseAuthenticatable
の認証が実行されるよ。参考
Scopeとは?
? Scopeを使うとUserやAdminなど、複数の認証ユーザーを利用できるようになるよ。Scope毎にSessionを分けて管理でき、適用するStrategyも分けることができるんだ。(認証ユーザーという語がちょっと紛らわしいけど、DeviseでいうとUserやAdminのような認証対象のモデルのイメージだよ。)
? デフォルトのScopeは
:default
で、Scopeが指定されていない場合は常に:default
になるよ。? Scopeはこんな感じで利用できるよ。
認証
# default Scope env['warden'].authenticated? # user Scope env['warden'].authenticated?(:user) # user Scope + password strategy env['warden'].authenticate(:password, scope: :user)ユーザー取得
# defaultユーザー env['warden'].user # userユーザー env['warden'].user(:user) # adminユーザー env['warden'].user(:admin)ログアウト
# 全ユーザーのsessionを削除 env['warden'].logout # defaultユーザーのsessionを削除 env['warden'].logout(:default) # userユーザーのsessionを削除 env['warden'].logout(:user)? Scopeの設定はこんな感じだよ。
use Warden::Manager do |manater| # userをデフォルトScopeにする manater.default_scope = :user # 各Scopeにstrategyを指定する manater.scope_defaults :user, :strategies => [:password] manater.scope_defaults :api, :store => false, :strategies => [:api_token], :action => "unauthenticated_api" end? DeviseではScopeは複数モデルを利用する際なんかに利用されるよ。詳しくは 023 複数モデルを利用する を参照してね。
参考
- Scopes · wardencommunity/warden Wiki · GitHub
- Authenticated session data · wardencommunity/warden Wiki · GitHub
参考
- Home · wardencommunity/warden Wiki · GitHub
- devise を知るにはまず warden を知るが良い - vimtaku blog
- Warden を使った認証のサンプル
- warden のコードリーディング - ogidowの日記
058 Rails Engine
?? Deviseってコントローラーやビューを作成してない状態でもログイン機能とか使えるよね?これってなんで?
? それはDeviseがRails Engine(以下Engine)という仕組みを使ってるからだよ。Engineを使うとホストとなるRailsアプリケーションに対して、gem形式で別のRailsアプリケーションを丸ごと提供できるんだ。Deviseではコントローラー・ビュー・ヘルパー・メーラーをEngineを使って提供しているよ。
? ホストアプリケーション(自前のRailsアプリケーション)では
Rails::Application
を継承したクラスを定義しているよね。config/application.rb
に定義されていて、そこで定義されているクラスがホストアプリケーション本体になるよ。conig/application.rbmodule DemoApp class Application < Rails::Application # ...省略... end end? コレに対してEngineでは
Rails::Engine
を継承したクラスを定義するよ。以下はDeviseの例だよ。lib/devise/rails.rbmodule Devise class Engine < ::Rails::Engine # ...省略... end end? この2つの構造は似ているよね。実際ApplicationとEngineは小さな違いを除けばほとんど同じものだよ。つまりDevise gemの中に、別のRailsアプリケーションがあって、それをホストアプリケーションから利用する感じになるんだね。
2つのEngine
? Engineは2つのタイプが存在するよ。
isolate_namespace
メソッドを使っているものと、使っていないもの。前者はmountMountable Engine、後者はFull Engineと呼ばれることもあるよ。isolate_namespaceありのEngine
?
isolate_namespace
を使うとEngineの名前空間をホストから切り分けることができ、ホストと疎なEngineにする事ができるよ。コントローラー名・モデル名・テーブル名に名前空間があるため、ホストアプリケーションと名前が衝突することがなくなるんだ。例えばEngine1
というアプリケーションのArticle
モデルはEngine1::Article
となるよ。?
isolate_namespace
を使って作られたEngineはRackアプリとしてmount可能だよ。こんな感じでホスト側からmountメソッドを使うことで、Engineにアクセスできるようになるよ。config/routes.rb# http://localhost:3000/engine1 でEngineにアクセス可能になる mount Engine1::Engine, at: "/engine1"? ホスト側のアプリケーションと分けて管理したい場合、例えば管理画面を作成する場合なんかに利用されるよ。
?
isolate_namespace
ありのEngineを作るには以下のコマンドを実行すればOKだよ。$ rails plugin new engine1 --mountable create create README.md create Rakefile create engine1.gemspec create MIT-LICENSE create .gitignore create Gemfile create app create app/controllers/engine1/application_controller.rb create app/helpers/engine1/application_helper.rb create app/jobs/engine1/application_job.rb create app/mailers/engine1/application_mailer.rb create app/models/engine1/application_record.rb create app/views/layouts/engine1/application.html.erb create app/assets/images/engine1 create app/assets/images/engine1/.keep create config/routes.rb create lib/engine1.rb create lib/tasks/engine1_tasks.rake create lib/engine1/version.rb create lib/engine1/engine.rb create app/assets/config/engine1_manifest.js create app/assets/stylesheets/engine1/application.css create bin/rails create test/test_helper.rb create test/engine1_test.rb append Rakefile create test/integration/navigation_test.rb vendor_app test/dummy? こんな感じで
isolate_namespace
を利用したEngineが作成されるよ。engine1/lib/engine1/engine.rbmodule Engine1 class Engine < ::Rails::Engine isolate_namespace Engine1 end end? 代表的なgemとしては、rails_adminが
isolate_namespace
ありのEngineを使っているよ。# ...省略... module RailsAdmin class Engine < Rails::Engine isolate_namespace RailsAdmin # ...省略... end end参考
isolate_namespaceなしのEngine
?
isolate_namespace
を使わない場合は名前空間なしになるよ。そのためホスト側と名前が衝突する可能性があるので命名には注意してね。? ルーティングに関してもわざわざホスト側でmountする必要はなくて、gemをinstallすればそのままホストアプリケーションからEngineにアクセスできるよ。
?
isolate_namespace
ありのEngineを作るには以下のコマンドを実行すればOKだよ。$ rails plugin new engine2 --full create create README.md create Rakefile create engine2.gemspec create MIT-LICENSE create .gitignore create Gemfile create app/models create app/models/.keep create app/controllers create app/controllers/.keep create app/mailers create app/mailers/.keep create app/assets/images/engine2 create app/assets/images/engine2/.keep create app/helpers create app/helpers/.keep create app/views create app/views/.keep create config/routes.rb create lib/engine2.rb create lib/tasks/engine2_tasks.rake create lib/engine2/version.rb create lib/engine2/engine.rb create app/assets/config/engine2_manifest.js create app/assets/stylesheets/engine2 create app/assets/stylesheets/engine2/.keep create bin/rails create test/test_helper.rb create test/engine2_test.rb append Rakefile create test/integration/navigation_test.rb vendor_app test/dummy?
isolate_namespace
なしのEngineが用意されるよ。engine2/lib/engine2/engine.rbmodule Engine2 class Engine < ::Rails::Engine end end? Deviseは
isolate_namespace
なしのEngineだよ。(ただしコントローラーに名前空間が用意されていたり、ルーティングをdevise_forメソッドで制御したりと、Mountable Engine的な動作をする)DeviseのEngine
? Deviseでどんな感じでEngineが利用されているか、実際のDeviseのコードを見ながら解説していくね。Deviseのコードは https://github.com/heartcombo/devise から見ることができるよ。
? Devise gemのプロジェクトルートはこんな感じになっているよ。
app/ bin/ config/ gemfiles/ guides/ lib/ test/ Gemfile.lock ISSUE_TEMPLATE.md MIT-LICENSE README.md Rakefile devise.png devise.gemspec CODE_OF_CONDUCT.md CONTRIBUTING.md Gemfile CHANGELOG.md .git/ .gitignore .travis.yml .yardopts? 一般的なgemのディレクトリ構成とそんなに変わらないね。でも1点だけ大きな違いがあるよ。gemの中に
app
ディレクトリが存在するんだ。app
ディレクトリの中を見ていくよ。appディレクトリ
? Railsアプリケーションのappディレクトリと同じように、Deviseのappディレクトリの中にはコントローラーやビューが置かれているよ。
app/controllers/ helpers/ mailers/ views/
? controllersディレクトリを見るよ。
app/controllers/devise/ devise_controller.rb
?
devise_controller.rb
とdeivse
ディレクトリがあるね。devise_controller.rb
のDeviseController
はDeviseの全コントローラーの親になるクラスだよ。app/controllers/devise_controller.rbclass DeviseController < Devise.parent_controller.constantize # ...省略... end?
devise
ディレクトリの中には、僕たちがホストアプリから実際に利用するコントローラー6つが置かれているよ。app/controllers/devise/confirmations_controller.rb omniauth_callbacks_controller.rb passwords_controller.rb registrations_controller.rb sessions_controller.rb unlocks_controller.rb
?
confirmations_controller.rb
を見てみるよ。app/controllers/devise/confirmations_controller.rbclass Devise::ConfirmationsController < DeviseController # GET /resource/confirmation/new def new self.resource = resource_class.new end # POST /resource/confirmation def create self.resource = resource_class.send_confirmation_instructions(resource_params) yield resource if block_given? if successfully_sent?(resource) respond_with({}, location: after_resending_confirmation_instructions_path_for(resource_name)) else respond_with(resource) end end # GET /resource/confirmation?confirmation_token=abcdef def show self.resource = resource_class.confirm_by_token(params[:confirmation_token]) yield resource if block_given? if resource.errors.empty? set_flash_message!(:notice, :confirmed) respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } else respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end protected # The path used after resending confirmation instructions. def after_resending_confirmation_instructions_path_for(resource_name) is_navigational_format? ? new_session_path(resource_name) : '/' end # The path used after confirmation. def after_confirmation_path_for(resource_name, resource) if signed_in?(resource_name) signed_in_root_path(resource) else new_session_path(resource_name) end end def translation_scope 'devise.confirmations' end end?
DeviseController
を継承したDevise::ConfirmationsController
が定義されているね。このDevise::ConfirmationsController
はConfirmableモジュールで利用するコントローラーで、各アクションはこんな感じだよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/confirmation devise/confirmations#show confirm。メールのリンク先はここ。クエリパラメーターのconfirmation_tokenが一致しないとアクセスできない。 GET /users/confirmation/new devise/confirmations#new confirm指示メール再送信画面。 POST /users/confirmation devise/confirmations#create confirm指示メール送信。 ? controllersディレクトリと同じように、viewsディレクトリ、helpersディレクトリ、mailersディレクトリにもこのEngineで利用するコードが置かれているよ。Engineの仕組みを使って、ホストアプリケーションからこのappディレクトリを利用することになるんだね。
Engineクラス
? Engineを用いたgemは、appディレクトリ以外にもう1つ、一般的なgemと違う点があるよ。
Rails::Engine
クラスを継承したEngine
クラスが定義されているんだ。Rails::Engine
を継承したクラスでは、ホストアプリケーションにアクセスしたり、Engineの初期化を行ったりできるよ。? Deviseでは
lib/devise/rails.rb
で定義されているよ。ここではcurrent_userなどのヘルパーメソッドの追加も行っているよ。lib/devise/rails.rb# frozen_string_literal: true require 'devise/rails/routes' require 'devise/rails/warden_compat' module Devise # Rails::Engineを継承した、Devise::Engineを定義 # Rails::Engineを継承すると、EngineがあることがgemからApplicationに通知される # ここではホストアプリケーションにアクセスしたり、Engineの初期化を行ったりできる class Engine < ::Rails::Engine config.devise = Devise # Warden::Managerをミドルウェアに追加 config.app_middleware.use Warden::Manager do |config| Devise.warden_config = config end # eager_load前にホストアプリケーションのルーティングをリロード config.before_eager_load do |app| app.reload_routes! if Devise.reload_routes end # current_userなどのヘルパーメソッドを追加 initializer "devise.url_helpers" do Devise.include_helpers(Devise::Controllers) end # Omniauthの設定 initializer "devise.omniauth", after: :load_config_initializers, before: :build_middleware_stack do |app| Devise.omniauth_configs.each do |provider, config| app.middleware.use config.strategy_class, *config.args do |strategy| config.strategy = strategy end end if Devise.omniauth_configs.any? Devise.include_helpers(Devise::OmniAuth) end end # secret_keyの設定 initializer "devise.secret_key" do |app| Devise.secret_key ||= Devise::SecretKeyFinder.new(app).find Devise.token_generator ||= if secret_key = Devise.secret_key Devise::TokenGenerator.new( ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key)) ) end end end end参考
059 Deviseコードリーディング
? Deviseはレールに乗っている場合は結構簡単にカスタマイズできるよ。でもレールから外れる場合、例えばコントローラーをカスタマイズしたい場合などには、Deviseのソースコードを読む必要がでてくるんだ。なのでDeviseがどんなディレクトリ構成になっていて、どんなコードが置かれているかを簡単にでも理解しておくと、カスタマイズの助けになるよ。
? Deviseのプロジェクトルートはこんな感じだよ。
app/ bin/ config/ gemfiles/ guides/ lib/ test/ Gemfile.lock ISSUE_TEMPLATE.md MIT-LICENSE README.md Rakefile devise.png devise.gemspec CODE_OF_CONDUCT.md CONTRIBUTING.md Gemfile CHANGELOG.md .git/ .gitignore .travis.yml .yardopts? この中でもとりわけ大事なのが
app/
とlib/
だよ。順に説明していくね。app/
? appディレクトリにはコントローラーやビューが置かれていて、これらはRails Engineとして独立したアプリケーションであるかのように機能するよ。
controllers/ helpers/ mailers/ views/app/controllers/
? controllersディレクトリ配下はこんな感じだよ。
$ tree app/controllers app/controllers ├── devise │ ├── confirmations_controller.rb # Confirmableモジュール用のコントローラー │ ├── omniauth_callbacks_controller.rb # Omniauthableモジュール用のコントローラー │ ├── passwords_controller.rb # Recoverableモジュール用のコントローラー │ ├── registrations_controller.rb # Registerableモジュール用のコントローラー │ ├── sessions_controller.rb # Database Authenticatableモジュール用のコントローラー │ └── unlocks_controller.rb # Lockableモジュール用のコントローラー └── devise_controller.rb # 親コントローラー 1 directory, 7 files?
devise_controller.rb
のDeviseController
はDeviseの各コントローラーの親となるクラスで、コントローラーに共通の処理はここに置かれているよ。app/controllers/# frozen_string_literal: true # All Devise controllers are inherited from here. class DeviseController < Devise.parent_controller.constantize include Devise::Controllers::ScopedViews if respond_to?(:helper) helper DeviseHelper end if respond_to?(:helper_method) helpers = %w(resource scope_name resource_name signed_in_resource resource_class resource_params devise_mapping) helper_method(*helpers) end prepend_before_action :assert_is_devise_resource! respond_to :html if mimes_for_respond_to.empty? # Override prefixes to consider the scoped view. # Notice we need to check for the request due to a bug in # Action Controller tests that forces _prefixes to be # loaded before even having a request object. # # This method should be public as it is in ActionPack # itself. Changing its visibility may break other gems. def _prefixes #:nodoc: @_prefixes ||= if self.class.scoped_views? && request && devise_mapping ["#{devise_mapping.scoped_path}/#{controller_name}"] + super else super end end protected # Gets the actual resource stored in the instance variable def resource instance_variable_get(:"@#{resource_name}") end # Proxy to devise map name def resource_name devise_mapping.name end alias :scope_name :resource_name # Proxy to devise map class def resource_class devise_mapping.to end # Returns a signed in resource from session (if one exists) def signed_in_resource warden.authenticate(scope: resource_name) end # Attempt to find the mapped route for devise based on request path def devise_mapping @devise_mapping ||= request.env["devise.mapping"] end # Checks whether it's a devise mapped resource or not. def assert_is_devise_resource! #:nodoc: unknown_action! <<-MESSAGE unless devise_mapping Could not find devise mapping for path #{request.fullpath.inspect}. This may happen for two reasons: 1) You forgot to wrap your route inside the scope block. For example: devise_scope :user do get "/some/route" => "some_devise_controller" end 2) You are testing a Devise controller bypassing the router. If so, you can explicitly tell Devise which mapping to use: @request.env["devise.mapping"] = Devise.mappings[:user] MESSAGE end # Returns real navigational formats which are supported by Rails def navigational_formats @navigational_formats ||= Devise.navigational_formats.select { |format| Mime::EXTENSION_LOOKUP[format.to_s] } end def unknown_action!(msg) logger.debug "[Devise] #{msg}" if logger raise AbstractController::ActionNotFound, msg end # Sets the resource creating an instance variable def resource=(new_resource) instance_variable_set(:"@#{resource_name}", new_resource) end # Helper for use in before_actions where no authentication is required. # # Example: # before_action :require_no_authentication, only: :new def require_no_authentication assert_is_devise_resource! return unless is_navigational_format? no_input = devise_mapping.no_input_strategies authenticated = if no_input.present? args = no_input.dup.push scope: resource_name warden.authenticate?(*args) else warden.authenticated?(resource_name) end if authenticated && resource = warden.user(resource_name) set_flash_message(:alert, 'already_authenticated', scope: 'devise.failure') redirect_to after_sign_in_path_for(resource) end end # Helper for use after calling send_*_instructions methods on a resource. # If we are in paranoid mode, we always act as if the resource was valid # and instructions were sent. def successfully_sent?(resource) notice = if Devise.paranoid resource.errors.clear :send_paranoid_instructions elsif resource.errors.empty? :send_instructions end if notice set_flash_message! :notice, notice true end end # Sets the flash message with :key, using I18n. By default you are able # to set up your messages using specific resource scope, and if no message is # found we look to the default scope. Set the "now" options key to a true # value to populate the flash.now hash in lieu of the default flash hash (so # the flash message will be available to the current action instead of the # next action). # Example (i18n locale file): # # en: # devise: # passwords: # #default_scope_messages - only if resource_scope is not found # user: # #resource_scope_messages # # Please refer to README or en.yml locale file to check what messages are # available. def set_flash_message(key, kind, options = {}) message = find_message(kind, options) if options[:now] flash.now[key] = message if message.present? else flash[key] = message if message.present? end end # Sets flash message if is_flashing_format? equals true def set_flash_message!(key, kind, options = {}) if is_flashing_format? set_flash_message(key, kind, options) end end # Sets minimum password length to show to user def set_minimum_password_length if devise_mapping.validatable? @minimum_password_length = resource_class.password_length.min end end def devise_i18n_options(options) options end # Get message for given def find_message(kind, options = {}) options[:scope] ||= translation_scope options[:default] = Array(options[:default]).unshift(kind.to_sym) options[:resource_name] = resource_name options = devise_i18n_options(options) I18n.t("#{options[:resource_name]}.#{kind}", **options) end # Controllers inheriting DeviseController are advised to override this # method so that other controllers inheriting from them would use # existing translations. def translation_scope "devise.#{controller_name}" end def clean_up_passwords(object) object.clean_up_passwords if object.respond_to?(:clean_up_passwords) end def respond_with_navigational(*args, &block) respond_with(*args) do |format| format.any(*navigational_formats, &block) end end def resource_params params.fetch(resource_name, {}) end ActiveSupport.run_load_hooks(:devise_controller, self) end? deviseディレクトリ配下に実際に利用するコントローラーが置かれているよ。例えば
sessions_controller.rb
のDevise::SessionsController
はDatabase Authenticableモジュールで利用するコントローラーで、ログイン/ログアウトのアクションが定義されているよ。ログイン/ログアウトのアクションをカスタマイズしたい場合なんかはこのコントローラーを参考にしながら、カスタムコントローラーを修正していくことになるよ。詳しくは 020 コントローラーをカスタマイズする を参照してね。app/controllers/sessions_controller.rb# frozen_string_literal: true class Devise::SessionsController < DeviseController prepend_before_action :require_no_authentication, only: [:new, :create] prepend_before_action :allow_params_authentication!, only: :create prepend_before_action :verify_signed_out_user, only: :destroy prepend_before_action(only: [:create, :destroy]) { request.env["devise.skip_timeout"] = true } # GET /resource/sign_in def new self.resource = resource_class.new(sign_in_params) clean_up_passwords(resource) yield resource if block_given? respond_with(resource, serialize_options(resource)) end # POST /resource/sign_in def create self.resource = warden.authenticate!(auth_options) set_flash_message!(:notice, :signed_in) sign_in(resource_name, resource) yield resource if block_given? respond_with resource, location: after_sign_in_path_for(resource) end # DELETE /resource/sign_out def destroy signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) set_flash_message! :notice, :signed_out if signed_out yield if block_given? respond_to_on_destroy end protected def sign_in_params devise_parameter_sanitizer.sanitize(:sign_in) end def serialize_options(resource) methods = resource_class.authentication_keys.dup methods = methods.keys if methods.is_a?(Hash) methods << :password if resource.respond_to?(:password) { methods: methods, only: [:password] } end def auth_options { scope: resource_name, recall: "#{controller_path}#new" } end def translation_scope 'devise.sessions' end private # Check if there is no signed in user before doing the sign out. # # If there is no signed in user, it will set the flash message and redirect # to the after_sign_out path. def verify_signed_out_user if all_signed_out? set_flash_message! :notice, :already_signed_out respond_to_on_destroy end end def all_signed_out? users = Devise.mappings.keys.map { |s| warden.user(scope: s, run_callbacks: false) } users.all?(&:blank?) end def respond_to_on_destroy # We actually need to hardcode this as Rails default responder doesn't # support returning empty response on GET request respond_to do |format| format.all { head :no_content } format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) } end end endapp/mailers/
? mailersディレクトリには
mailer.rb
があるだけだよ。$ tree app/mailers app/mailers └── devise └── mailer.rb 1 directory, 1 file? ここではDeivseのメーラーである
Devise::Mailer
クラスが定義されているよ。こちらはコントローラーとは違い、モジュール毎に分かれているわけではなく、5つのメールが全てこのクラスに定義されているよ。各メールはtokenを用意するかどうかが違うくらいで、あとは共通の処理になっているね。app/mailers/devise/mailer.rb# frozen_string_literal: true if defined?(ActionMailer) class Devise::Mailer < Devise.parent_mailer.constantize include Devise::Mailers::Helpers def confirmation_instructions(record, token, opts = {}) @token = token devise_mail(record, :confirmation_instructions, opts) end def reset_password_instructions(record, token, opts = {}) @token = token devise_mail(record, :reset_password_instructions, opts) end def unlock_instructions(record, token, opts = {}) @token = token devise_mail(record, :unlock_instructions, opts) end def email_changed(record, opts = {}) devise_mail(record, :email_changed, opts) end def password_change(record, opts = {}) devise_mail(record, :password_change, opts) end end endapp/views/
? viewsディレクトリはこんな感じだよ。コントローラーとメーラーのビューが置かれているよ。
$ tree app/views app/views └── devise ├── confirmations │ └── new.html.erb ├── mailer │ ├── confirmation_instructions.html.erb │ ├── email_changed.html.erb │ ├── password_change.html.erb │ ├── reset_password_instructions.html.erb │ └── unlock_instructions.html.erb ├── passwords │ ├── edit.html.erb │ └── new.html.erb ├── registrations │ ├── edit.html.erb │ └── new.html.erb ├── sessions │ └── new.html.erb ├── shared │ ├── _error_messages.html.erb │ └── _links.html.erb └── unlocks └── new.html.erb 8 directories, 14 files? このビューは
$ rails g devise:views
コマンドでコピーするビューと同じものだよ。ビューをアプリ内にコピーした場合はアプリ内のビューを使い、コピーしない場合はこちらのgem内のビューを利用することになるよ。?
shared
ディレクトリにはパーシャルが置かれているよ。_error_messages.html.erb
はバリデーションエラーの表示だよ。devise/app/views/devise/shared/_error_messages.html.erb<% if resource.errors.any? %> <div id="error_explanation"> <h2> <%= I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %> </h2> <ul> <% resource.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %>?
_links.html.erb
はサインアップやログインなどのリンクを集めたものだよ。現在のpathや有効なモジュールによって表示するリンク変えているよ。devise/app/views/devise/shared/_links.html.erb<%- if controller_name != 'sessions' %> <%= link_to "Log in", new_session_path(resource_name) %><br /> <% end %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> <%= link_to "Sign up", new_registration_path(resource_name) %><br /> <% end %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%= link_to "Forgot your password?", new_password_path(resource_name) %><br /> <% end %> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br /> <% end %> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br /> <% end %> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br /> <% end %> <% end %>app/helpers/
? helpersディレクトリには
devise_helper.rb
があり、ここにはビューヘルパーが置かれているよ。devise_error_messages!
はバリデーションエラーを表示するヘルパーなのだけど、現在はバリデーションエラー表示のパーシャルを直接レンダリングするように変更されていて、後方互換性を維持するために残されているよ。app/helpers/devise_helper.rbmodule DeviseHelper # Retain this method for backwards compatibility, deprecated in favor of modifying the # devise/shared/error_messages partial. def devise_error_messages! ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc [Devise] `DeviseHelper#devise_error_messages!` is deprecated and will be removed in the next major version. Devise now uses a partial under "devise/shared/error_messages" to display error messages by default, and make them easier to customize. Update your views changing calls from: <%= devise_error_messages! %> to: <%= render "devise/shared/error_messages", resource: resource %> To start customizing how errors are displayed, you can copy the partial from devise to your `app/views` folder. Alternatively, you can run `rails g devise:views` which will copy all of them again to your app. DEPRECATION return "" if resource.errors.empty? render "devise/shared/error_messages", resource: resource end end? appディレクトリはこんな感じだね。
lib/
? lib配下はとても大きくて全部は説明できないので、知っておくと役に立ちそうな箇所をピックアップしていくね。
lib/devise/
? 基本的にコアとなるコードは
lib/devise/
配下に置かれているよ。lib/devise/controllers/
? コントローラーで利用するヘルパーが定義されているよ。
lib/devise/controllers/rememberable.rb scoped_views.rb sign_in_out.rb store_location.rb url_helpers.rb helpers.rb
? 例として
sign_in_out.rb
を見てみるよ。sign_in
やsign_out
などの、ログイン/ログアウトに関するコントローラーヘルパーが定義されているよ。lib/devise/controllers/sign_in_out.rb# frozen_string_literal: true module Devise module Controllers # Provide sign in and sign out functionality. # Included by default in all controllers. module SignInOut # Return true if the given scope is signed in session. If no scope given, return # true if any scope is signed in. This will run authentication hooks, which may # cause exceptions to be thrown from this method; if you simply want to check # if a scope has already previously been authenticated without running # authentication hooks, you can directly call `warden.authenticated?(scope: scope)` def signed_in?(scope = nil) [scope || Devise.mappings.keys].flatten.any? do |_scope| warden.authenticate?(scope: _scope) end end # Sign in a user that already was authenticated. This helper is useful for logging # users in after sign up. All options given to sign_in is passed forward # to the set_user method in warden. # If you are using a custom warden strategy and the timeoutable module, you have to # set `env["devise.skip_timeout"] = true` in the request to use this method, like we do # in the sessions controller: https://github.com/heartcombo/devise/blob/master/app/controllers/devise/sessions_controller.rb#L7 # # Examples: # # sign_in :user, @user # sign_in(scope, resource) # sign_in @user # sign_in(resource) # sign_in @user, event: :authentication # sign_in(resource, options) # sign_in @user, store: false # sign_in(resource, options) # def sign_in(resource_or_scope, *args) options = args.extract_options! scope = Devise::Mapping.find_scope!(resource_or_scope) resource = args.last || resource_or_scope expire_data_after_sign_in! if options[:bypass] ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller) [Devise] bypass option is deprecated and it will be removed in future version of Devise. Please use bypass_sign_in method instead. Example: bypass_sign_in(user) DEPRECATION warden.session_serializer.store(resource, scope) elsif warden.user(scope) == resource && !options.delete(:force) # Do nothing. User already signed in and we are not forcing it. true else warden.set_user(resource, options.merge!(scope: scope)) end end # Sign in a user bypassing the warden callbacks and stores the user straight in session. This option is useful in cases the user is already signed in, but we want to refresh the credentials in session. # # Examples: # # bypass_sign_in @user, scope: :user # bypass_sign_in @user def bypass_sign_in(resource, scope: nil) scope ||= Devise::Mapping.find_scope!(resource) expire_data_after_sign_in! warden.session_serializer.store(resource, scope) end # Sign out a given user or scope. This helper is useful for signing out a user # after deleting accounts. Returns true if there was a logout and false if there # is no user logged in on the referred scope # # Examples: # # sign_out :user # sign_out(scope) # sign_out @user # sign_out(resource) # def sign_out(resource_or_scope = nil) return sign_out_all_scopes unless resource_or_scope scope = Devise::Mapping.find_scope!(resource_or_scope) user = warden.user(scope: scope, run_callbacks: false) # If there is no user warden.logout(scope) warden.clear_strategies_cache!(scope: scope) instance_variable_set(:"@current_#{scope}", nil) !!user end # Sign out all active users or scopes. This helper is useful for signing out all roles # in one click. This signs out ALL scopes in warden. Returns true if there was at least one logout # and false if there was no user logged in on all scopes. def sign_out_all_scopes(lock = true) users = Devise.mappings.keys.map { |s| warden.user(scope: s, run_callbacks: false) } warden.logout expire_data_after_sign_out! warden.clear_strategies_cache! warden.lock! if lock users.any? end private def expire_data_after_sign_in! # session.keys will return an empty array if the session is not yet loaded. # This is a bug in both Rack and Rails. # A call to #empty? forces the session to be loaded. session.empty? session.keys.grep(/^devise\./).each { |k| session.delete(k) } end alias :expire_data_after_sign_out! :expire_data_after_sign_in! end end endlib/devise/models/
? Userモデルに追加されるメソッドがモジュール毎に定義されているよ。
lib/devise/models/confirmable.rb database_authenticatable.rb lockable.rb omniauthable.rb recoverable.rb registerable.rb rememberable.rb timeoutable.rb trackable.rb validatable.rb authenticatable.rb
? 例として
trackable.rb
を見てみるよ。IPアドレス・ログイン時刻・ログイン回数を更新するメソッドが定義されているよ。ここで定義されているメソッドはuser.update_tracked_fields!(request)
のようにして利用できるよ。lib/devise/models/trackable.rb# frozen_string_literal: true require 'devise/hooks/trackable' module Devise module Models # Track information about your user sign in. It tracks the following columns: # # * sign_in_count - Increased every time a sign in is made (by form, openid, oauth) # * current_sign_in_at - A timestamp updated when the user signs in # * last_sign_in_at - Holds the timestamp of the previous sign in # * current_sign_in_ip - The remote ip updated when the user sign in # * last_sign_in_ip - Holds the remote ip of the previous sign in # module Trackable def self.required_fields(klass) [:current_sign_in_at, :current_sign_in_ip, :last_sign_in_at, :last_sign_in_ip, :sign_in_count] end def update_tracked_fields(request) old_current, new_current = self.current_sign_in_at, Time.now.utc self.last_sign_in_at = old_current || new_current self.current_sign_in_at = new_current old_current, new_current = self.current_sign_in_ip, extract_ip_from(request) self.last_sign_in_ip = old_current || new_current self.current_sign_in_ip = new_current self.sign_in_count ||= 0 self.sign_in_count += 1 end def update_tracked_fields!(request) # We have to check if the user is already persisted before running # `save` here because invalid users can be saved if we don't. # See https://github.com/heartcombo/devise/issues/4673 for more details. return if new_record? update_tracked_fields(request) save(validate: false) end protected def extract_ip_from(request) request.remote_ip end end end endlib/devise/strategies/
? WardenのStrategyを利用した認証が定義されているよ。
lib/devise/strategies/authenticatable.rb base.rb database_authenticatable.rb rememberable.rb
? 例として
database_authenticatable.rb
を見てみるね。ここではDatabase Authenticatableモジュール用のStrategyが定義されているよ。authenticate!
メソッドを見ると、emailとpasswordを利用して認証していることがわかるよ。lib/devise/strategies/database_authenticatable.rb# frozen_string_literal: true require 'devise/strategies/authenticatable' module Devise module Strategies # Default strategy for signing in a user, based on their email and password in the database. class DatabaseAuthenticatable < Authenticatable def authenticate! resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash) hashed = false if validate(resource){ hashed = true; resource.valid_password?(password) } remember_me(resource) resource.after_database_authentication success!(resource) end # In paranoid mode, hash the password even when a resource doesn't exist for the given authentication key. # This is necessary to prevent enumeration attacks - e.g. the request is faster when a resource doesn't # exist in the database if the password hashing algorithm is not called. mapping.to.new.password = password if !hashed && Devise.paranoid unless resource Devise.paranoid ? fail(:invalid) : fail(:not_found_in_database) end end end end end Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)lib/devise/hooks/
? WardenのCallback機能を利用したhook処理が定義されているよ。WardenのCallback機能については Callbacks · wardencommunity/warden Wiki · GitHub を参照してね。
lib/devise/hooks/csrf_cleaner.rb forgetable.rb lockable.rb proxy.rb rememberable.rb timeoutable.rb trackable.rb activatable.rb
? 例として
trackable.rb
を見てみるよ。Trackableモジュールが有効な場合、ログイン時(after_set_user時)にIPアドレス・ログイン時刻・ログイン回数を更新していることがわかるね。lib/devise/hooks/trackable.rb# frozen_string_literal: true # After each sign in, update sign in time, sign in count and sign in IP. # This is only triggered when the user is explicitly set (with set_user) # and on authentication. Retrieving the user from session (:fetch) does # not trigger it. Warden::Manager.after_set_user except: :fetch do |record, warden, options| if record.respond_to?(:update_tracked_fields!) && warden.authenticated?(options[:scope]) && !warden.request.env['devise.skip_trackable'] record.update_tracked_fields!(warden.request) end endlib/devise/test/
? テストで利用するヘルパーが定義されているよ。
lib/devise/test/controller_helpers.rb integration_helpers.rb
? 例として
integration_helpers.rb
を見てみるね。ここにはIntegrationテストに対してヘルパーを提供するDevise::Test::IntegrationHelpers
モジュールが定義されているよ。このモジュールをincludeすることでテストでsign_in
とsign_out
が利用できるようになるよ。lib/devise/test/integration_helpers.rb# frozen_string_literal: true module Devise # Devise::Test::IntegrationHelpers is a helper module for facilitating # authentication on Rails integration tests to bypass the required steps for # signin in or signin out a record. # # Examples # # class PostsTest < ActionDispatch::IntegrationTest # include Devise::Test::IntegrationHelpers # # test 'authenticated users can see posts' do # sign_in users(:bob) # # get '/posts' # assert_response :success # end # end module Test module IntegrationHelpers def self.included(base) base.class_eval do include Warden::Test::Helpers setup :setup_integration_for_devise teardown :teardown_integration_for_devise end end # Signs in a specific resource, mimicking a successful sign in # operation through +Devise::SessionsController#create+. # # * +resource+ - The resource that should be authenticated # * +scope+ - An optional +Symbol+ with the scope where the resource # should be signed in with. def sign_in(resource, scope: nil) scope ||= Devise::Mapping.find_scope!(resource) login_as(resource, scope: scope) end # Signs out a specific scope from the session. # # * +resource_or_scope+ - The resource or scope that should be signed out. def sign_out(resource_or_scope) scope = Devise::Mapping.find_scope!(resource_or_scope) logout scope end protected def setup_integration_for_devise Warden.test_mode! end def teardown_integration_for_devise Warden.test_reset! end end end endlib/devise/rails.rb
? DeviseのRails Engineが定義されているよ。
lib/devise/rails.rb# frozen_string_literal: true require 'devise/rails/routes' require 'devise/rails/warden_compat' module Devise class Engine < ::Rails::Engine config.devise = Devise # Initialize Warden and copy its configurations. config.app_middleware.use Warden::Manager do |config| Devise.warden_config = config end # Force routes to be loaded if we are doing any eager load. config.before_eager_load do |app| app.reload_routes! if Devise.reload_routes end initializer "devise.url_helpers" do Devise.include_helpers(Devise::Controllers) end initializer "devise.omniauth", after: :load_config_initializers, before: :build_middleware_stack do |app| Devise.omniauth_configs.each do |provider, config| app.middleware.use config.strategy_class, *config.args do |strategy| config.strategy = strategy end end if Devise.omniauth_configs.any? Devise.include_helpers(Devise::OmniAuth) end end initializer "devise.secret_key" do |app| Devise.secret_key ||= Devise::SecretKeyFinder.new(app).find Devise.token_generator ||= if secret_key = Devise.secret_key Devise::TokenGenerator.new( ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(secret_key)) ) end end end endlib/devise/rails/routes.rb
?
devise_for
やdevise_scope
などのルーティングで利用するメソッドが定義されているよ。lib/devise/rails/routes.rb# ...省略... def devise_for(*resources) @devise_finalized = false raise_no_secret_key unless Devise.secret_key options = resources.extract_options! options[:as] ||= @scope[:as] if @scope[:as].present? options[:module] ||= @scope[:module] if @scope[:module].present? options[:path_prefix] ||= @scope[:path] if @scope[:path].present? options[:path_names] = (@scope[:path_names] || {}).merge(options[:path_names] || {}) options[:constraints] = (@scope[:constraints] || {}).merge(options[:constraints] || {}) options[:defaults] = (@scope[:defaults] || {}).merge(options[:defaults] || {}) options[:options] = @scope[:options] || {} options[:options][:format] = false if options[:format] == false resources.map!(&:to_sym) resources.each do |resource| mapping = Devise.add_mapping(resource, options) begin raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise) rescue NameError => e raise unless mapping.class_name == resource.to_s.classify warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \ "no model #{mapping.class_name} defined in your application" next rescue NoMethodError => e raise unless e.message.include?("undefined method `devise'") raise_no_devise_method_error!(mapping.class_name) end if options[:controllers] && options[:controllers][:omniauth_callbacks] unless mapping.omniauthable? raise ArgumentError, "Mapping omniauth_callbacks on a resource that is not omniauthable\n" \ "Please add `devise :omniauthable` to the `#{mapping.class_name}` model" end end routes = mapping.used_routes devise_scope mapping.name do with_devise_exclusive_scope mapping.fullpath, mapping.name, options do routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) } end end end end # Allow you to add authentication request from the router. # Takes an optional scope and block to provide constraints # on the model instance itself. # # authenticate do # resources :post # end # # authenticate(:admin) do # resources :users # end # # authenticate :user, lambda {|u| u.role == "admin"} do # root to: "admin/dashboard#show", as: :user_root # end # def authenticate(scope = nil, block = nil) constraints_for(:authenticate!, scope, block) do yield end end # Allow you to route based on whether a scope is authenticated. You # can optionally specify which scope and a block. The block accepts # a model and allows extra constraints to be done on the instance. # # authenticated :admin do # root to: 'admin/dashboard#show', as: :admin_root # end # # authenticated do # root to: 'dashboard#show', as: :authenticated_root # end # # authenticated :user, lambda {|u| u.role == "admin"} do # root to: "admin/dashboard#show", as: :user_root # end # # root to: 'landing#show' # def authenticated(scope = nil, block = nil) constraints_for(:authenticate?, scope, block) do yield end end # Allow you to route based on whether a scope is *not* authenticated. # You can optionally specify which scope. # # unauthenticated do # as :user do # root to: 'devise/registrations#new' # end # end # # root to: 'dashboard#show' # def unauthenticated(scope = nil) constraint = lambda do |request| not request.env["warden"].authenticate? scope: scope end constraints(constraint) do yield end end # Sets the devise scope to be used in the controller. If you have custom routes, # you are required to call this method (also aliased as :as) in order to specify # to which controller it is targeted. # # as :user do # get "sign_in", to: "devise/sessions#new" # end # # Notice you cannot have two scopes mapping to the same URL. And remember, if # you try to access a devise controller without specifying a scope, it will # raise ActionNotFound error. # # Also be aware of that 'devise_scope' and 'as' use the singular form of the # noun where other devise route commands expect the plural form. This would be a # good and working example. # # devise_scope :user do # get "/some/route" => "some_devise_controller" # end # devise_for :users # # Notice and be aware of the differences above between :user and :users def devise_scope(scope) constraint = lambda do |request| request.env["devise.mapping"] = Devise.mappings[scope] true end constraints(constraint) do yield end end # ...省略...lib/generators/
? コントローラーやビューなどを作成するためのジェネレーターが置かれているよ。
lib/devies.rb
?
Devise
モジュールが定義されていて、モジュールのautoloadを行っているよ。lib/devies.rb# frozen_string_literal: true require 'rails' require 'active_support/core_ext/numeric/time' require 'active_support/dependencies' require 'orm_adapter' require 'set' require 'securerandom' require 'responders' module Devise autoload :Delegator, 'devise/delegator' autoload :Encryptor, 'devise/encryptor' autoload :FailureApp, 'devise/failure_app' autoload :OmniAuth, 'devise/omniauth' autoload :ParameterFilter, 'devise/parameter_filter' autoload :ParameterSanitizer, 'devise/parameter_sanitizer' autoload :TestHelpers, 'devise/test_helpers' autoload :TimeInflector, 'devise/time_inflector' autoload :TokenGenerator, 'devise/token_generator' autoload :SecretKeyFinder, 'devise/secret_key_finder' module Controllers autoload :Helpers, 'devise/controllers/helpers' autoload :Rememberable, 'devise/controllers/rememberable' autoload :ScopedViews, 'devise/controllers/scoped_views' autoload :SignInOut, 'devise/controllers/sign_in_out' autoload :StoreLocation, 'devise/controllers/store_location' autoload :UrlHelpers, 'devise/controllers/url_helpers' end module Hooks autoload :Proxy, 'devise/hooks/proxy' end module Mailers autoload :Helpers, 'devise/mailers/helpers' end module Strategies autoload :Base, 'devise/strategies/base' autoload :Authenticatable, 'devise/strategies/authenticatable' end module Test autoload :ControllerHelpers, 'devise/test/controller_helpers' autoload :IntegrationHelpers, 'devise/test/integration_helpers' end # ...省略... end? あと各設定のデフォルト値もここでセットされているよ。
lib/devies.rb# ...省略... # Secret key used by the key generator mattr_accessor :secret_key @@secret_key = nil # Custom domain or key for cookies. Not set by default mattr_accessor :rememberable_options @@rememberable_options = {} # The number of times to hash the password. mattr_accessor :stretches @@stretches = 12 # The default key used when authenticating over http auth. mattr_accessor :http_authentication_key @@http_authentication_key = nil # Keys used when authenticating a user. mattr_accessor :authentication_keys @@authentication_keys = [:email] # Request keys used when authenticating a user. mattr_accessor :request_keys @@request_keys = [] # Keys that should be case-insensitive. mattr_accessor :case_insensitive_keys @@case_insensitive_keys = [:email] # Keys that should have whitespace stripped. mattr_accessor :strip_whitespace_keys @@strip_whitespace_keys = [:email] # If http authentication is enabled by default. mattr_accessor :http_authenticatable @@http_authenticatable = false # If http headers should be returned for ajax requests. True by default. mattr_accessor :http_authenticatable_on_xhr @@http_authenticatable_on_xhr = true # If params authenticatable is enabled by default. mattr_accessor :params_authenticatable @@params_authenticatable = true # The realm used in Http Basic Authentication. mattr_accessor :http_authentication_realm @@http_authentication_realm = "Application" # Email regex used to validate email formats. It asserts that there are no # @ symbols or whitespaces in either the localpart or the domain, and that # there is a single @ symbol separating the localpart and the domain. mattr_accessor :email_regexp @@email_regexp = /\A[^@\s]+@[^@\s]+\z/ # Range validation for password length mattr_accessor :password_length @@password_length = 6..128 # The time the user will be remembered without asking for credentials again. mattr_accessor :remember_for @@remember_for = 2.weeks # If true, extends the user's remember period when remembered via cookie. mattr_accessor :extend_remember_period @@extend_remember_period = false # If true, all the remember me tokens are going to be invalidated when the user signs out. mattr_accessor :expire_all_remember_me_on_sign_out @@expire_all_remember_me_on_sign_out = true # Time interval you can access your account before confirming your account. # nil - allows unconfirmed access for unlimited time mattr_accessor :allow_unconfirmed_access_for @@allow_unconfirmed_access_for = 0.days # Time interval the confirmation token is valid. nil = unlimited mattr_accessor :confirm_within @@confirm_within = nil # Defines which key will be used when confirming an account. mattr_accessor :confirmation_keys @@confirmation_keys = [:email] # Defines if email should be reconfirmable. mattr_accessor :reconfirmable @@reconfirmable = true # Time interval to timeout the user session without activity. mattr_accessor :timeout_in @@timeout_in = 30.minutes # Used to hash the password. Please generate one with rails secret. mattr_accessor :pepper @@pepper = nil # Used to send notification to the original user email when their email is changed. mattr_accessor :send_email_changed_notification @@send_email_changed_notification = false # Used to enable sending notification to user when their password is changed. mattr_accessor :send_password_change_notification @@send_password_change_notification = false # Scoped views. Since it relies on fallbacks to render default views, it's # turned off by default. mattr_accessor :scoped_views @@scoped_views = false # Defines which strategy can be used to lock an account. # Values: :failed_attempts, :none mattr_accessor :lock_strategy @@lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account mattr_accessor :unlock_keys @@unlock_keys = [:email] # Defines which strategy can be used to unlock an account. # Values: :email, :time, :both mattr_accessor :unlock_strategy @@unlock_strategy = :both # Number of authentication tries before locking an account mattr_accessor :maximum_attempts @@maximum_attempts = 20 # Time interval to unlock the account if :time is defined as unlock_strategy. mattr_accessor :unlock_in @@unlock_in = 1.hour # Defines which key will be used when recovering the password for an account mattr_accessor :reset_password_keys @@reset_password_keys = [:email] # Time interval you can reset your password with a reset password key mattr_accessor :reset_password_within @@reset_password_within = 6.hours # When set to false, resetting a password does not automatically sign in a user mattr_accessor :sign_in_after_reset_password @@sign_in_after_reset_password = true # The default scope which is used by warden. mattr_accessor :default_scope @@default_scope = nil # Address which sends Devise e-mails. mattr_accessor :mailer_sender @@mailer_sender = nil # Skip session storage for the following strategies mattr_accessor :skip_session_storage @@skip_session_storage = [:http_auth] # Which formats should be treated as navigational. mattr_accessor :navigational_formats @@navigational_formats = ["*/*", :html] # When set to true, signing out a user signs out all other scopes. mattr_accessor :sign_out_all_scopes @@sign_out_all_scopes = true # The default method used while signing out mattr_accessor :sign_out_via @@sign_out_via = :delete # The parent controller all Devise controllers inherits from. # Defaults to ApplicationController. This should be set early # in the initialization process and should be set to a string. mattr_accessor :parent_controller @parent_controller = "ApplicationController" # The parent mailer all Devise mailers inherit from. # Defaults to ActionMailer::Base. This should be set early # in the initialization process and should be set to a string. mattr_accessor :parent_mailer @parent_mailer = "ActionMailer::Base" # The router Devise should use to generate routes. Defaults # to :main_app. Should be overridden by engines in order # to provide custom routes. mattr_accessor :router_name @@router_name = nil # Set the OmniAuth path prefix so it can be overridden when # Devise is used in a mountable engine mattr_accessor :omniauth_path_prefix @@omniauth_path_prefix = nil # Set if we should clean up the CSRF Token on authentication mattr_accessor :clean_up_csrf_token_on_authentication @@clean_up_csrf_token_on_authentication = true # When false, Devise will not attempt to reload routes on eager load. # This can reduce the time taken to boot the app but if your application # requires the Devise mappings to be loaded during boot time the application # won't boot properly. mattr_accessor :reload_routes @@reload_routes = true # PRIVATE CONFIGURATION # Store scopes mappings. mattr_reader :mappings @@mappings = {} # OmniAuth configurations. mattr_reader :omniauth_configs @@omniauth_configs = {} # Define a set of modules that are called when a mapping is added. mattr_reader :helpers @@helpers = Set.new @@helpers << Devise::Controllers::Helpers # Private methods to interface with Warden. mattr_accessor :warden_config @@warden_config = nil @@warden_config_blocks = [] # When true, enter in paranoid mode to avoid user enumeration. mattr_accessor :paranoid @@paranoid = false # When true, warn user if they just used next-to-last attempt of authentication mattr_accessor :last_attempt_warning @@last_attempt_warning = true # Stores the token generator mattr_accessor :token_generator @@token_generator = nil # When set to false, changing a password does not automatically sign in a user mattr_accessor :sign_in_after_change_password @@sign_in_after_change_password = true # ...省略...第14章 認証gemの比較
時間切れ。時間できたら書きます。
060 Devise
061 Sorcery
062 Clearance
063 Authlogic
064 認証gemの比較まとめ
Deviseの情報源
日本語の情報源
? DeviseのReadMeの翻訳だよ。
DeviseのREADMEを翻訳してみた - Qiita
Railsの第4世代認証エンジンDeviseのREADMEを翻訳してみた - babie, you're my home? Deviseの基本的な使い方が詳しく書かれているよ。
[Rails] deviseの使い方(rails6版) - Qiita
#209 Introducing Devise - RailsCasts
#210 Customizing Devise - RailsCasts? TechRachoさんのDeviseのWikiのまとめだよ。DeviseのWikiはページ数が多いので、目当てのページを探す際に役立つよ。
[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
[Rails] Devise Wiki日本語もくじ2「認証方法のカスタマイズ」「OmniAuth」(概要・用途付き)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
[Rails] Devise Wiki日本語もくじ3「ビュー/コンテンツのカスタマイズ」「特権/認証」|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
[Rails] Devise Wiki日本語もくじ4「テスト」「特殊な設定」|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
[Rails] Devise Wiki日本語もくじ5「アプリでのその他の設定」「JavaScript」|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
[Rails] Devise Wiki日本語もくじ6「他の認証プラグインからの移行」「アップグレード」|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社英語の情報源
? 本家リポジトリだよ。
GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.
? 本家Wikiだよ。カスタマイズ方法が充実してるけど、ページ数が多いので全部読むのはなかなか大変だよ。TechRachoさんのまとめから、目当てのページを探すのがおすすめだよ。
Home · heartcombo/devise Wiki · GitHub
チートシート
後から見返す用のまとめ
コントローラー・ビューのメソッド
# リクエストしてきたユーザーを認証する。 # ユーザーがログイン済みの場合はアクセスを許可して、未ログインの場合はroot_pathにリダイレクトする。 # コントローラーで`before_action :authenticate_user!`の形で利用する。 authenticate_user! # ログイン済みの場合はログインユーザーを返す。 current_user # ログイン済みの場合はtrueを返す。 user_signed_in? # ユーザーに紐づくsessionを返す。 user_session # Deviseのコントローラーだったらtrueを返す。 devise_controller? # ログインさせる。 sign_in(user) # Wardenのコールバックをバイパスしてログインさせる。 # ユーザーが認証情報を変更した際にログアウトしてしまうので、それを防ぐために利用する。 bypass_sign_in(user) # ログアウトさせる。 sign_out(user)ルーティングのメソッド
# 有効なモジュールに対応するルーティングを定義する。 devise_for :users # 独自のルーティングを定義する。 devise_scope :user do get "sign_in", to: "devise/sessions#new" end # ログイン後のルーティングを定義する。 authenticated do root to: 'dashboard#show', as: :authenticated_root end # ログイン前のルーティングを定義する。 unauthenticated do root to: 'dashboard2#show', as: :unauthenticated_root end # 認証付きルーティングを定義する。 authenticate do resources :cats endモデルのメソッド
# ==> Database Authenticatableモジュール # passwordをセットする。 # 内部で暗号化して`encrypted_password`にセットしてくれるよ。 user.password = "password" # パスワードが正しければtrue。 # 引数のパスワードをハッシュ化してencrypted_passwordの値と比較してくれる。 user.valid_password?('password') #=> true # passwordとpassword_confirmationにnilをセット。 user.clean_up_passwords # ==> Rememberableモジュール # remember_tokenを作成 user.remember_me! # remember_tokenを削除 user.forget_me! # user情報を使ってcookieを作成 User.serialize_into_cookie(user) # cookie情報を使ってuserを取得 User.serialize_from_cookie(cookie_string) # ==> Recoverableモジュール # パスワードリセットメール送信 user.send_reset_password_instructions # パスワードリセット # user.reset_password(new_password, new_password_confirmation) user.reset_password('password123', 'password123') # reset_password_tokenが有効期限内かどうかを、reset_password_sent_atを使い判定 user.reset_password_period_valid? #=> true # tokenを使ってuserを取得 User.with_reset_password_token(token) #=> user # ==> Timeoutableモジュール # タイムアウトならtrue user.timedout?(Time.current) # ==> Lockableモジュール # ロック(メール送信もする) user.lock_access! # ロック(メール送信しない) user.lock_access!(send_instructions: false) # アンロック user.unlock_access! # アンロックのメール送信 user.resend_unlock_instructions # ==> Confirmableモジュール # confirmする # 具体的にはconfirmed_atに現在時刻を設定する user.confirm # confirm済みなら、true user.confirmed? # 手動でConfirmメールを送信 user.send_confirmation_instructionsリンク(path)
ビュー<!-- ログイン前 --> <%= link_to("サインアップ", new_user_registration_path) %> <%= link_to("ログイン", new_user_session_path) %> <%= link_to("パスワードをお忘れですか?", new_user_password_path) %> <%= link_to("アカウント確認のメールを受け取っていませんか?", new_user_confirmation_path) %> <%= link_to("アンロック指示のメールを受け取っていませんか?", new_user_unlock_path) %> <!-- ログイン後 --> <%= link_to("ログアウト", destroy_user_session_path, method: :delete) %> <%= link_to("アカウント編集", edit_user_registration_path) %> <%= link_to("アカウント削除", user_registration_path, method: :delete) %>モジュール
モジュール名 機能 デフォルト Registerable サインアップ機能 有効 Database Authenticatable Email/Password入力によるログイン機能 有効 Rememberable Remember Me機能(ブラウザを閉じてもログインが継続する機能) 有効 Recoverable パスワードリセット機能 有効 Validatable Email/Passwordのバリデーション機能 有効 Confirmable サインアップ時に本登録用のメールを送信して、メールアドレスを確認する機能 無効 Trackable ログイン時の情報(IPアドレスなど)をDBに保存する機能 無効 Timeoutable 一定期間アクセスがないと強制ログアウトさせる機能 無効 Lockable 指定回数ログイン失敗でアカウントをロックする機能 無効 Omniauthable Omniauthとの連携機能(Twitter・Googleアカウントなどでログインできる) 無効 コントローラーとルーティング
Registerableモジュール
HTTPメソッド path コントローラーアクション 目的 GET /users/sign_up devise/registrations#new サインアップ画面 GET /users/edit devise/registrations#edit アカウント編集画面。emailやpasswordを編集できる。 POST /users devise/registrations#create アカウント登録 PATCH/PUT /users devise/registrations#update アカウント更新 DELETE /users devise/registrations#destroy アカウント削除 GET /users/cancel devise/registrations#cancel session削除。OAuthのsessionデータを削除したい場合に使う。 Database Authenticatableモジュール
HTTPメソッド path コントローラー#アクション 目的 GET /users/sign_in devise/sessions#new ログイン画面 POST /users/sign_in devise/sessions#create ログイン DELETE /users/sign_out devise/sessions#destroy ログアウト Recoverableモジュール
HTTPメソッド path コントローラー#アクション 目的 GET /users/password/new devise/passwords#new パスワードリセットのメール送信画面 GET /users/password/edit devise/passwords#edit パスワード再設定画面 POST /users/password devise/passwords#create パスワードリセットのメール送信 PATCH/PUT /users/password devise/passwords#update パスワード再設定 Confirmableモジュール
HTTPメソッド path コントローラー#アクション 目的 GET /users/confirmation devise/confirmations#show confirm。
メールのリンク先はここ。
クエリパラメーターのconfirmation_tokenが一致しないとアクセスできない。GET /users/confirmation/new devise/confirmations#new confirm指示メール再送信画面。 POST /users/confirmation devise/confirmations#create confirm指示メール送信。 Lockableモジュール
HTTPメソッド path コントローラー#アクション 目的 GET /users/unlock devise/unlocks#show アンロック。
メールのリンク先はここ。
クエリパラメーターのunlock_tokenが一致しないとアクセスできない。GET /users/unlock/new devise/unlocks#new アンロック指示メール再送信画面。 POST /users/unlock devise/unlocks#create アンロック指示メール送信。 カラム
Database Authenticatableモジュール
カラム 概要 メールアドレス。
認証に利用。
DB的にはユニークキーになり、ユーザーは重複するメールアドレスを登録することができないよ。encrypted_password ハッシュ化されたパスワード。
認証に利用。
パスワードを直接DBに保存するのはセキュリティー的に問題があるので、ハッシュ化したパスワードをDBに保存するよ。Deviseでは内部的にbcryptというハッシュ化関数を使っていて、DB保存前に自動的にハッシュ化してくれるよ。Rememberableモジュール
カラム 概要 remember_created_at Remenber Meした時刻 remember_token remember_me用のtoken
remember_tokenカラムがなければ、encrypted_passwordの先頭30文字で代用するので、別になくてもOKだよ。マイグレーションファイルにも記載されないよ。Recoverableモジュール
カラム 概要 reset_password_token パスワードリセットで利用するトークン。
一意のランダムなトークンが生成される。
パスワードリセットメールからパスワード再設定画面(/users/password/edit
)へアクセスする際に、ユーザーを判定するのに利用する。reset_password_sent_at パスワードリセットメール送信時刻。
パスワードリセットメールの有効期限の判定に利用する。Confirmableモジュール
カラム 概要 confirmation_token confirmする際に利用するトークン。
一意のランダムなトークンが生成される。
confirm指示メールからconfirmアクション(/users/confirmattion
)へアクセスする際に、ユーザーを判定するのに利用する。confirmed_at confirmされた時刻。
confirm済みかどうかはこのカラムがnilかどうかで判定する。confirmation_sent_at confirmation_token作成時刻。 unconfirmed_email まだconfirmされていないメールアドレス。
email変更時のconfirmで利用する。config.unconfirmed_email = true
の場合だけ必要。
confirmされるまでは新しいはemailはこのカラムに保存され、confirm時にemailのカラムにコピーされる。Trackableモジュール
カラム 概要 sign_in_count ログイン回数 current_sign_in_at 最新のログイン時刻 last_sign_in_at 1つ前のログイン時刻 current_sign_in_ip 最新のログイン時IPアドレス last_sign_in_ip 1つ前のログイン時IPアドレス Lockableモジュール
カラム 概要 failed_attempts 失敗回数。 config.lock_strategy = :failed_attempts
の場合にだけ必要。unlock_token メールからアンロックする際に利用するtoken。
一意のランダムなトークンが生成される。
アンロック指示メールからアンロックアクション(/users/unlock
)へアクセスする際に、ユーザーを判定するのに利用する。config.unlock_strategy
が:both
の場合にだけ必要。locked_at ロック時刻。
これがnullでない場合にロック状態とみなされる。メール
Database Authenticatable
メーラー#メソッド 概要 Devise::Mailer#email_changed Eメール変更完了メール。Eメール変更時に送信する。 Devise::Mailer#password_change パスワード変更完了メール。パスワード変更時に送信する。 Recoverable
メーラー#メソッド 概要 Devise::Mailer#reset_password_instructions パスワードリセットメール Confirmable
メーラー#メソッド 概要 Devise::Mailer#confirmation_instructions confirm指示メール Lockable
メーラー#メソッド 概要 Devise::Mailer#unlock_instructions アカウントアンロック指示メール ジェネレーター
# 設定ファイルとロケールファイルを作成する。 $ rails g devise:install # モデルを作成する。 # User以外も指定可能。 $ rails g devise User $ rails g devise Admin # ビューをコピーする。 # Scopeを指定可能。Scopeを指定しない場合は`devise`名前空間のビューが作成される。 # `-v`オプションで指定ビューだけをコピーできる。 $ rails g devise:views $ rails g devise:views user $ rails g devise:views -v registrations confirmations # コントローラーを作成する。 # ビューと違いScope指定は必須。 # `-c`オプションで指定コントローラーだけ作成。 $ rails g devise:controllers users $ rails g devise:controllers users -c=sessions設定
config/initializers/devise.rb# frozen_string_literal: true Devise.setup do |config| # Deviseが使用する秘密鍵。 # Deviseはこのキーを利用してtokenを作成する(confirmation_token、reset_password_token、unlock_token)。 # このキーを変更すると全てのtokenが無効になる。 # デフォルトではsecret_key_baseをsecret_keyとして利用する。 # config.secret_key = '48bf747d05636bd17b63751533ac6879106a058e94253754a0bfe552d60ab822ad52c25b322c93b90d7479a91fe28da84ac038f8b295d523a4c2a18c08ed9c42' # ==> Controllerの設定 # Devise::SessionsControllerなどのDeviseの各コントローラーの親クラス。 # config.parent_controller = 'DeviseController' # ==> Mailerの設定 # Mailerのfrom。 config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' # Mailerクラス # カスタムMailerを利用する場合はここを変更する。 # 詳細は『035 メーラーをカスタマイズする』を参照。 # config.mailer = 'Devise::Mailer' # Devise::Mailerの親クラス。 # config.parent_mailer = 'ActionMailer::Base' # ==> ORMの設定 # ORMをロードする。 # ActiveRcordとMongoidをサポートしている。 require 'devise/orm/active_record' # ==> 認証全般の設定 # 認証キー(ユーザーを認証する際に利用するキー)。 # email以外のキーを利用したい場合に変更する。 # 詳細は『024 emailの代わりにusernameでログインさせる』を参照。 # config.authentication_keys = [:email] # 認証に使用するリクエストオブジェクトのパラメータ。 # config.request_keys = [] # 大文字小文字を区別しない認証キー。 # Userの作成/修正/認証/検索時に大文字小文字を区別しない 。 config.case_insensitive_keys = [:email] # 空白を削除する認証キー。 # Userの作成/修正/認証/検索時に空白を削除する。 config.strip_whitespace_keys = [:email] # request.paramsによる認証を有効にする。 # `config.params_authenticatable = [:database]`とすればDB認証(メール + パスワード)認証のみを有効にする。 # config.params_authenticatable = true # HTTP Authによる認証を有効にする。 # `config.http_authenticatable = [:database]` とすればDB認証のみを有効にする。 # config.http_authenticatable = false # Ajaxリクエストに対して401を返す。 # config.http_authenticatable_on_xhr = true # Basic認証で利用されるrealm。 # config.http_authentication_realm = 'Application' # paranoidモード。 # メールアドレスが登録されているかどうかを確認するのを防ぐ。 # 詳細は https://github.com/heartcombo/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable # config.paranoid = true # userをsessionに保存する処理をスキップする箇所。 config.skip_session_storage = [:http_auth] # セキュリティーのため認証時にCSRFトークンをsessionから削除する。 # trueだとサインインやサインアップでAjaxを使用する場合、サーバーから新しいCSRFトークンを取得する必要がある。 # config.clean_up_csrf_token_on_authentication = true # eager load時にルーティングをリロードする。 # before_eager_loadフックを利用。 # falseにするとアプリ起動が高速になるが、Deviseのマッピングをロードする必要がある場合は正常に起動できない。 # config.reload_routes = true # ==> Database Authenticatableモジュールの設定 # ハッシュ化のレベル。 # ハッシュ化には結構時間がかかる。 # bcrypt(デフォルトのアルゴリズム)の場合、レベルに応じて指数関数的に遅くなり、例えばレベル20では60秒程度かかる。 # テストの時はレベル1にして速度を上げる。 # 本番ではレベル10以下は利用すべきでない。 config.stretches = Rails.env.test? ? 1 : 12 # ハッシュ化する際のpepper。 # pepperはsaltみたいなやつ。 # 詳細は https://stackoverflow.com/questions/6831796/whats-the-most-secure-possible-devise-configuration # config.pepper = '9a11b4eaf0250fec05630de0b518c3f63086fa403a8309d74408b3223d57a2312cef3ef746152f43c508da74b11cf21f982d9573ef552a186e36d83818129029' # email変更時にemail変更完了メールを送信する。 # config.send_email_changed_notification = false # password変更時にpassword変更完了メールを送信する。 # config.send_password_change_notification = false # ==> Confirmableモジュールの設定 # confirmなしでログインできる期間。 # これを設定すると一定期間はconfirm前でもログインできるようになる。 # nilに設定すると無期限にログインできるようになる。 # デフォルトは 0.days。(confirmなしにはログインできない。) # config.allow_unconfirmed_access_for = 2.days # confirmation_tokenの有効期限。 # ユーザーはこの期限内にconfirm指示メールのリンクをクリックしないといけない。 # デフォルトは nil。(制限なし。) # config.confirm_within = 3.days # サインアップ時だけでなく、email変更時にもConfirmメールを送信する。 # unconfirmed_emailカラムが必要。 config.reconfirmable = true # confirmのキー。 # config.confirmation_keys = [:email] # ==> Rememberableモジュールの設定 # Sessionが切れるまでの時間。 # デフォルトは2.weeks。 # config.remember_for = 2.weeks # ログアウト時にremember_tokenを期限切れにする。 config.expire_all_remember_me_on_sign_out = true # cookie利用時に期間を伸ばす。 # config.extend_remember_period = false # cookieにセットするオプション。 # config.rememberable_options = {} # ==> Validatableモジュールの設定 # passwordの長さ。 # Rangeで指定。この場合は6文字から128文字。 config.password_length = 6..128 # emailバリデーションで利用する正規表現 config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # ==> Timeoutableモジュールの設定 # タイムアウト時間 # config.timeout_in = 30.minutes # ==> lockableモジュールの設定 # ロック方法 # - failed_attempts: 指定回数間違えたらロック # - none: 自動ロックはなしで、サーバ管理者が手動でロック # config.lock_strategy = :failed_attempts # アンロックのキー # config.unlock_keys = [:email] # アンロック方法 # - email: メールでアンロックのリンクを送信 # - time: 数時間後にアンロック(config.unlock_inと一緒に使う) # - both: emailとtimeの両方 # - none: 自動アンロックはなしで、サーバ管理者が手動でアンロック # config.unlock_strategy = :both # ロックまでの回数 # config.maximum_attempts = 20 # アンロックまでの時間(`config.unlock_strategy = :time`の場合) # config.unlock_in = 1.hour # ロック前に警告する # config.last_attempt_warning = true # ==> Recoverableモジュールの設定 # # パスワードリセット時にキーになるカラム。 # config.reset_password_keys = [:email] # パスワードリセットの有効期限。 config.reset_password_within = 6.hours # パスワードリセット後に自動ログイン。 # config.sign_in_after_reset_password = true # ==> devise-encryptable gemの設定 # bcrypt以外のハッシュ化アルゴリズム。 # devise-encryptable gemのインストールが必要。 # bcrypt以外のアルゴリズムは:sha1、:sha512、:clearance_sha1、:authlogic_sha512、:sha1など。 # config.encryptor = :sha512 # ==> Scopeの設定 # Scope用のビューを優先的に使うようになる。 # trueにすると`devise`名前空間のビューではなく、`users`などのScope対応のビューを利用する。 # デフォルトは高速化のため`false`に設定されている。 # 詳細は『023 複数モデルを利用する』を参照。 # config.scoped_views = false # デフォルトのScope。 # 通常であればuserになる。 # config.default_scope = :user # ログアウト時に全てのScopeでのログアウトとする。 # falseの場合は/users/sign_outでログアウトした場合、user scopeだけログアウトになる。 # config.sign_out_all_scopes = true # ==> Navigationの設定 # ナビゲーションとして扱われるフォーマットのリスト。 # config.navigational_formats = ['*/*', :html] # ログアウト時のHTTPメソッド config.sign_out_via = :delete # ==> OmniAuthの設定 # OmniAuthの設定。 # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' # ==> Wardenの設定 # Wardenの設定。 # strategy追加したりfailure_app変更したり。 # # config.warden do |manager| # manager.intercept_401 = false # manager.default_strategies(scope: :user).unshift :some_external_strategy # end # ==> Mountable Engineの設定 # Mountable Engineで使う際のrouter名。 # config.router_name = :my_engine # # OmniAuthのpath。 # OmniAuthを利用する場合に設定する。 # config.omniauth_path_prefix = '/my_engine/users/auth' # ==> Turbolinksの設定 # Turbolinksを利用している場合、リダイレクトを正しく動作させるためにTurbolinks::Controllerをincludeする。 # # ActiveSupport.on_load(:devise_failure_app) do # include Turbolinks::Controller # end # ==> Registerableモジュールの設定 # パスワード変更後に自動的にサインインさせる。 # config.sign_in_after_change_password = true end
- 投稿日:2020-12-01T11:55:26+09:00
Devise入門64のレシピ その1
これは「フィヨルドブートキャンプ Advent Calendar 2020」の1日目の記事です。
フィヨルドブートキャンプ Part 1 Advent Calendar 2020 - Adventar
フィヨルドブートキャンプ Part 2 Advent Calendar 2020 - Adventarqiitaの文字数制限(?)に引っかかるようなので記事を分割しました。
Devise入門64のレシピ その1
Devise入門64のレシピ その2目次
- 環境
- 第1章 Deviseをはじめよう
- 第2章 モジュールを使う
- 第3章 ビューをカスタマイズする
- 第4章 コントローラーをカスタマイズする
- 第5章 モデルをカスタマイズする
- 第6章 ルーティングをカスタマイズする
- 第7章 メーラーをカスタマイズする
- 第8章 I18nをカスタマイズする
- 第9章 設定をカスタマイズする
- 第10章 その他のカスタマイズ(Wikiまとめ)
- 第11章 Tips
- 第12章 Deviseのエコシステム
- 第13章 Devise内部を知る
- 第14章 認証gemの比較
- Deviseの情報源
- チートシート
環境
Ruby: 2.7.1
Rails: 6.0.3
Devise: 4.7.3第1章 Deviseをはじめよう
? DeviseはRailsに認証機能を提供するgemだよ。Deviseを使うとユーザーはサインアップやログインができるようになるよ。
001 Deviseを使ってみよう
? Deviseがどんな感じなのか実際に使ってみよう!
? まずはDeviseをinstallするよ。
Gemfilegem "devise"$ bundle install? 次は
$ rails g devise:install
コマンドを実行してね。Deviseの設定ファイル(devise.rb
)とロケールファイル(devise.en.yml
)が作成されて、英語でセットアップの指示が表示されるよ。$ rails g devise:install create config/initializers/devise.rb create config/locales/devise.en.yml =============================================================================== Depending on your application's configuration some manual setup may be required: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. * Required for all applications. * 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" * Not required for API-only Applications * 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> * Not required for API-only Applications * 4. You can copy Devise views (for customization) to your app by running: rails g devise:views * Not required * ===============================================================================? 指示通りにセットアップを進めていくよ。まずはActionMailerにデフォルトURLを設定するよ。Deviseはパスワードリセットなどでユーザーにメールを送信するのだけど、これを設定しておくことでメール内のリンクを正しく表示できるようになるよ。開発環境と本番環境の設定ファイルにそれぞれ設定してね。
config/environments/development.rb# 開発環境はこのままコピペでいいよ config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }config/environments/production.rb# 本番環境ではhost名を指定してね config.action_mailer.default_url_options = { host: 'xxxx.com' }? 次はrootのルーティングを設定するよ。ここでは
HomeController
のindex
アクションを設定するよ。config/routes.rbroot to: "home#index"?
HomeController
を作成しておくね。$ rails g controller home index? flashメッセージを表示できるようにするよ。これを設定しておくと、ユーザーがログインした時などに『ログインしました。』のようなメッセージを画面に表示できるようになるよ。レイアウトアウトテンプレートに
notice
とalert
を表示するコードを追加してね。app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>DemoApp</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <!-- この2行を追加してね --> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> <%= yield %> </body> </html>? 次はDeviseのビューファイルを自分のプロジェクトにコピーするよ。このままでもDeviseは動くけど、コピーしておくことでビューファイルを自由にカスタマイズできるようになるよ。
$ rails g devise:views invoke Devise::Generators::SharedViewsGenerator create app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for create app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb create app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb create app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb create app/views/devise/sessions create app/views/devise/sessions/new.html.erb create app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb create app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb? Userモデルを作成するよ。
$ rails g devise User
を実行すると、Userモデルとusersテーブルを作成するためのmigrationファイルが作成されるよ。$ rails g devise User invoke active_record create db/migrate/20201103065100_devise_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml insert app/models/user.rb route devise_for :users? db:migrateを実行してusersテーブルを作成してね。
$ rails db:migrate? これで完了だよ。サーバーを起動して、ブラウザから
http://localhost:3000/users/sign_in
にアクセスするとログイン画面が表示されるよ。$ rails server? ログインだけでなく、サインアップなどの機能も使えるので遊んでみてね。
002 ヘルパーを使ってみよう
? Deviseはコントローラーとビューで使えるヘルパーメソッドを提供してくれるよ。
?
HomeController
に以下のコードを追加してね。app/controllers/home_controller.rbclass HomeController < ApplicationController # リクエストしてきたユーザーを認証する。 # ユーザーがログイン済みの場合はアクセスを許可して、未ログインの場合はroot_pathにリダイレクトする。 before_action :authenticate_user! def index end end?
before_action :authenticate_user!
を利用することで、HomeController
へのアクセスを認証できるようになるよ。もしユーザーが未ログインだったらこのコントローラーにはアクセスできずに、root_pathへリダイレクトされることになるよ。? 指定のアクションだけ認証したい場合は
only
オプションを使えばOKだよ。app/controllers/home_controller.rbclass HomeController < ApplicationController before_filter :authenticate_user!, only: %i(index) def index end def new end end? これでnewアクションは認証しないので、未ログイン状態でもアクセスできるよ。
? 他にも
user_signed_in?
やcurrent_user
などのメソッドが追加されるよ。app/controllers/home_controller.rbclass HomeController < ApplicationController def index # user_signed_in?: ログイン済みの場合はtrueを返す。 # current_user: ログイン済みの場合はログインユーザーを返す。 # ログイン済みの場合、ログインユーザーのidをログに書き込む。 if user_signed_in? logger.debug current_user.id end # ...省略... end end? これらのヘルパーを使ってアプリケーションを開発していくことになるよ。他のヘルパーについては コントローラー・ビューのメソッド を参照してね。
第2章 モジュールを使う
003 モジュールとは?
? 認証では『ログイン』以外にも、『サインアップ』や『パスワードリセット』など、いろんな機能が必要になるよね。Deviseは認証の各機能をモジュールとして提供しているよ。例えば『ログイン時に何度もパスワードを間違えた場合は、アカウントをロックしたい。』みたいな場合がある。そんな時はLockableモジュールを有効にしてあげれば、自分でコードを書かなくてもDeviseがアカウントロックの機能を追加してくれるんだ。アプリケーションによって要件は変わるけれども、Deviseは各機能がモジュール形式になっているので、必要なモジュールだけを選んで使うことができるよ。
モジュールの種類
? モジュールは全部で10個あるよ。
モジュール名 機能 デフォルト Registerable サインアップ機能 有効 Database Authenticatable Email/Password入力によるログイン機能 有効 Rememberable Remember Me機能(ブラウザを閉じてもログインが継続する機能) 有効 Recoverable パスワードリセット機能 有効 Validatable Email/Passwordのバリデーション機能 有効 Confirmable サインアップ時に本登録用のメールを送信して、メールアドレスを確認する機能 無効 Trackable ログイン時の情報(IPアドレスなど)をDBに保存する機能 無効 Timeoutable 一定期間アクセスがないと強制ログアウトさせる機能 無効 Lockable 指定回数ログイン失敗でアカウントをロックする機能 無効 Omniauthable Omniauthとの連携機能(Twitter・Googleアカウントなどでログインできる) 無効 ? モジュールはUserモデルの
devise
メソッドで指定すると有効にできるんだ。デフォルトではdatabase_authenticatable
、registerable
、recoverable
、rememberable
、validatable
の5つのモジュールが有効になっているよ。app/models/user.rbclass User < ApplicationRecord # 以下の5つのモジュールはデフォルトでは無効だよ。 # :confirmable, :lockable, :timeoutable, :trackable, :omniauthable # 以下の5つのモジュールがデフォルトで有効だよ。 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end? デフォルトで有効になっている5つのモジュールは、特別な事情がない限りそのまま有効にしておけばいいと思うよ。デフォルトで無効になっている5つのモジュールは必要に応じて有効にしてね。
モジュールのカラム
? モジュールによってはusersテーブルにカラムを追加する必要があるよ。
?
rails g devise:install
コマンドで作成されたマイグレーションファイルを見てみるよ。db/migrate/20201103065100_devise_create_users.rb# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[6.0] 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 null: false 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? こんな感じでモジュール毎に必要なカラムが用意されているよ。例えばConfirmableモジュールを有効にしたい場合は、コメントアウトされている
confirmation_token
などをアンコメントする必要があるよ。db/migrate/20201103065100_devise_create_users.rb# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[6.0] 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 null: false 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? これでマイグレーションを実行すればモジュールに必要なカラムを追加できるよ。
$ rails db:migrate? 後からモジュールを追加する場合は、カラムを追加するマイグレーションファイルを作成すればOKだよ。
$ rails g migration add_confirmable_columns_to_users invoke active_record create db/migrate/20201115225427_add_confirmable_columns_to_users.rbdb/migrate/20201115225427_add_confirmable_columns_to_users.rbclass AddConfirmableColumnsToUsers < ActiveRecord::Migration[6.0] def change change_table :users do |t| # Confirmableに必要なカラム t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.string :unconfirmed_email end add_index :users, :confirmation_token, unique: true end endモジュールのルーティング
? Deviseのルーティングは
devise_for
メソッドが用意してくれるよ。config/routes.rbRails.application.routes.draw do devise_for :users end? モジュールを有効にすると、
devise_for
メソッドによってモジュールに対応するルーティングが自動的に追加されるよ。デフォルトでは5つのモジュールが有効になっているので、それに対応するルーティングが追加されているよ。ただ、全てのモジュールにコントローラーがあるわけではなく、今回であればdatabase_authenticatable
、registerable
、recoverable
の3つのモジュールにコントローラーが存在するんだ。そのためこの3つのコントローラーに対応するルーティングが追加されるよ。$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel new_user_registration GET /users/sign_up(.:format) devise/registrations#new edit_user_registration GET /users/edit(.:format) devise/registrations#edit user_registration PATCH /users(.:format) devise/registrations#update PUT /users(.:format) devise/registrations#update DELETE /users(.:format) devise/registrations#destroy POST /users(.:format) devise/registrations#create? Confirmableモジュールを有効にすると、Confirmableモジュール用のルーティングが追加されるよ。
app/models/user.rbclass User < ApplicationRecord # Confirmableモジュールを追加する devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable end$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel new_user_registration GET /users/sign_up(.:format) devise/registrations#new edit_user_registration GET /users/edit(.:format) devise/registrations#edit user_registration PATCH /users(.:format) devise/registrations#update PUT /users(.:format) devise/registrations#update DELETE /users(.:format) devise/registrations#destroy POST /users(.:format) devise/registrations#create # Confirmableモジュール用のルーティングが追加される new_user_confirmation GET /users/confirmation/new(.:format) devise/confirmations#new user_confirmation GET /users/confirmation(.:format) devise/confirmations#show POST /users/confirmation(.:format) devise/confirmations#create? ルーティングは有効なモジュールによって自動で決まるので、
routes.rb
の編集は不要だよ。モジュールのコントローラーとビュー
? モジュールによってはコントローラーとビューを提供するものもあるよ。例えばConfirmableモジュールは
Devise::ConfirmationsController
とそれに対応するビューを提供するよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/confirmation devise/confirmations#show confirm。メールのリンク先はここ。クエリパラメーターのconfirmation_tokenが一致しないとアクセスできない。 GET /users/confirmation/new devise/confirmations#new confirm指示メール再送信画面。 POST /users/confirmation devise/confirmations#create confirm指示メール送信。 モジュールのメソッド
? モジュールを追加すると
User
にメソッドが追加されるよ。例えばConfirmableモジュールを追加すると、確認メールを送信するためのUser#send_confirmation_instructions
メソッドなどが追加されるよ。通常であればユーザーに対する操作は用意されたDeviseのコントローラーから行うので、これらのメソッドを直接使うことは少ないよ。ただ手動で操作したい場合には、直接これらのメソッドを使うことになるよ。# 手動でConfirmメールを送信 user.send_confirmation_instructions # confirmする # 具体的にはconfirmed_atに現在時刻を設定する user.confirm # confirm済みなら、true user.confirmed?モジュールのメール送信
? メールを送信するためにActionMailerを利用するモジュールもあるよ。例えばConfirmableモジュールであれば確認メールである
Devise::Mailer#confirmation_instructions
を提供するよ。モジュールの設定
? 各モジュールは専用の設定があり、設定を変更することでモジュールの挙動を変更できるよ。
config/initializers/devise.rb# Confirmableモジュールの設定 # 確認メールの有効期限 config.confirm_within = 3.days004 Registerableモジュール
? ここからは各モジュールの解説をしていくね。
? Registerableモジュールはサインアップ機能を提供するよ。具体的にはUserレコードを作成/更新/削除する機能を提供するよ。
コントローラーとルーティング
? Registerableモジュールでは
Devise::RegistrationsController
というコントローラーと以下の6つのアクションが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/sign_up devise/registrations#new サインアップ画面 GET /users/edit devise/registrations#edit アカウント編集画面。emailやpasswordを編集できる。 POST /users devise/registrations#create アカウント登録 PATCH/PUT /users devise/registrations#update アカウント更新 DELETE /users devise/registrations#destroy アカウント削除 GET /users/cancel devise/registrations#cancel session削除。OAuthのsessionデータを削除したい場合に使う。 ?
/users/sign_up
を開くとサインアップ画面が表示されるよ。? Registration(登録)というリソースをnew(新規作成)するため
Devise::RegistrationsController#new
になっているよ。pathはRestfulに考えると/registration/new
になるのだけど、サインアップ画面であることがわかりやすくなるように/users/sign_up
となっているよ。? サインアップするとUserレコードが作成されて、サインイン状態になり、アカウント編集画面(
/users/edit
)にリダイレクトされるよ。アカウント編集画面ではユーザーが自分のアカウント情報であるemailやpasswordを編集できるよ。設定
? Registerableモジュールで設定できる項目は以下の通りだよ。
config/initializers/devise.rb# パスワード変更後に自動的にサインインさせる。 config.sign_in_after_change_password = true参考
005 Database Authenticatableモジュール
? Database Authenticatableモジュールを使うと、emailとpasswordでログインできるようになるよ。
?? emailとpassword以外のログイン方法もあるってこと?
? そうだよ。後ででてくるけど、例えばOmniauthableモジュールを使えば、TwitterやGoogleのアカウントを使ってログインできるようになるよ。ここではDatabase Authenticatableモジュールを使ったemailとpasswordでのログインについて解説するね。
コントローラーとルーティング
? Database Authenticatableモジュールでは
Devise::SessionsController
というコントローラーと以下の3つのアクションが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/sign_in devise/sessions#new ログイン画面 POST /users/sign_in devise/sessions#create ログイン DELETE /users/sign_out devise/sessions#destroy ログアウト ? ブラウザから
localhost:3000/users/sign_in
を開くとログイン画面が表示されるよ。? Deviseではログイン時にSessionを作成するよ。
Session
をリソースだと考えて、それをnew
(新規作成)するため、ログイン画面のアクションはDevise::SessionsController#new
となっているよ。pathはログイン画面だということがわかりやすいように、/session/new
ではなく/users/sign_in
となっているよ。? ログイン後に
Devise::SessionsController#destroy
を叩けばログアウトできるよ。こんな感じのリンクを用意してね。<!-- destroy_user_session_pathは`/users/sign_out`を指すURLヘルパーだよ --> <!-- HTTPメソッドにはDELETEメソッドを指定してね --> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>カラム
? Database Authenticatableモジュールでは、usersテーブルに以下の2つのカラムが必要になるよ。
カラム 概要 メールアドレス。
認証に利用。
DB的にはユニークキーになり、ユーザーは重複するメールアドレスを登録することができないよ。encrypted_password ハッシュ化されたパスワード。
認証に利用。
パスワードを直接DBに保存するのはセキュリティー的に問題があるので、ハッシュ化したパスワードをDBに保存するよ。Deviseでは内部的にbcryptというハッシュ化関数を使っていて、DB保存前に自動的にハッシュ化してくれるよ。設定
? Database Authenticatableモジュールで設定できる項目は以下の通りだよ。
config/initializers/devise.rb# ハッシュ化のレベル。 # ハッシュ化には結構時間がかかる。 # bcrypt(デフォルトのアルゴリズム)の場合レベルに応じて指数関数的に遅くなり、例えばレベル20では60秒程度かかる。 # テストの時はレベル1にして速度を上げる。 # 本番ではレベル10以下は利用すべきでない。 config.stretches = Rails.env.test? ? 1 : 11 # ハッシュ化する際のペッパー。(saltみたいなやつ。) # 詳細は https://stackoverflow.com/questions/6831796/whats-the-most-secure-possible-devise-configuration config.pepper = 'e343ec013eac51040db52ee0cc22175d262f8bd87badc7ec87dcba597ccde6e4449b7890bba62d8598fd8f33b0ffbb7ad128ee5e39a18509691851cbfc81b80a' # email変更時にemail変更完了メールを送信する。 config.send_email_changed_notification = false # password変更時にpassword変更完了メールを送信する。 config.send_password_change_notification = falseメソッド
? Database AuthenticatableモジュールではUserモデルに以下のメソッドを提供するよ。
# passwordをセットする。 # 内部で暗号化して`encrypted_password`にセットしてくれるよ。 user.password = "password" user.encrypted_password #=> "$2a$12$V/xUMhmLEZApbyv2Y0jI4eyJ0gYE8JlVPL2/1Yr9jcFXChnQzC0Hi" # パスワードが正しければtrue。 # 引数のパスワードをハッシュ化してencrypted_passwordの値と比較してくれる。 user.valid_password?('password') #=> true # passwordとpassword_confirmationにnilをセット。 user.clean_up_passwords user.password #=> nil user.password_confirmation #=> nilメール
? Database Authenticatableモジュールでは以下の2つのメールを送信するよ。
メーラー#メソッド 概要 Devise::Mailer#email_changed Eメール変更完了メール。Eメール変更時に送信する。 Devise::Mailer#password_change パスワード変更完了メール。パスワード変更時に送信する。 ? この2つはデフォルトではメール送信しない設定になっているので、もしメール送信したい場合は設定を変更してね。
config/initializers/devise.rb# email変更時にemail変更完了メールを送信する。 config.send_email_changed_notification = true # password変更時にpassword変更完了メールを送信する。 config.send_password_change_notification = true参考
006 Rememberableモジュール
? RememberableモジュールはRemember Me機能を提供するよ。Cookieにユーザーのトークンを保存することで、セッションが切れてもCookieからユーザーを取得して、ログイン状態を維持できるよ。
? Rememberableモジュールが有効だと、ログイン画面にRemember meというチェックボックスが用意されるよ。ユーザーはここにチェックを入れてログインすることで、Remember Meを利用できるんだ。
カラム
カラム 概要 remember_created_at Remenber Meした時刻 remember_token remember_me用のtoken
remember_tokenカラムがなければ、encrypted_passwordの先頭30文字で代用するので、別になくてもOKだよ。マイグレーションファイルにも記載されないよ。設定
config/initializers/devise.rb# Sessionが切れるまでの時間。 # デフォルトは2.weeks。 config.remember_for = 2.weeks # ログアウト時にremember_tokenを期限切れにする。 config.expire_all_remember_me_on_sign_out = true # cookie利用時に期間を伸ばす。 config.extend_remember_period = false # cookieにセットするオプション。 config.rememberable_options = {secure: true}メソッド
# remember_tokenを作成 user.remember_me! # remember_tokenを削除 user.forget_me! # user情報を使ってcookieを作成 User.serialize_into_cookie(user) # cookie情報を使ってuserを取得 User.serialize_from_cookie(cookie_string)参考
- devise/rememberable.rb at master · heartcombo/devise · GitHub
- Devise3.2.2 のデフォルト設定では、Rememberable の remember_token のカラムがないのでソースを解読してみた | EasyRamble
007 Recoverableモジュール
? Recoverableモジュールはパスワードリセット機能を提供するよ。パスワードを忘れてログインできないユーザーのために、パスワードを再設定できるリンクをメールで送信できるよ。Recoverableモジュールはログイン前にパスワードを変更する機能なので、ログイン後にパスワードを変更したい場合はRegisterableモジュールのアカウント編集機能(
/users/edit
)を使ってね。? 実際にどんな機能か使ってみるね。ログイン画面に行くと一番下に『Forgot your password?』というリンクがあるからここをクリックしてね。
? パスワードリセットのメール送信画面(
/users/password/new
)に遷移するよ。ここでパスワードリセットしたいアカウントのメールアドレスを入力してsubmitしてね。? すると入力したメールアドレス宛に、こんなメールが送信されるよ。
? メール内の『Change my password』というリンクをクリックしてね。パスワード再設定画面(
/users/password/edit
)に遷移するよ。? パスワードと確認用パスワードを入力してsubmitすると、パスワードが再設定されて、ログイン状態になるよ。
コントローラーとルーティング
? Recoverableモジュールでは
Devise::PasswordsController
というコントローラーと以下の4つのアクションが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/password/new devise/passwords#new パスワードリセットのメール送信画面 GET /users/password/edit devise/passwords#edit パスワード再設定画面 POST /users/password devise/passwords#create パスワードリセットのメール送信 PATCH/PUT /users/password devise/passwords#update パスワード再設定 カラム
? Recoverableモジュールでは、usersテーブルに以下の2つのカラムが必要になるよ。
カラム 概要 reset_password_token パスワードリセットで利用するトークン。
一意のランダムなトークンが生成される。
パスワードリセットメールからパスワード再設定画面(/users/password/edit
)へアクセスする際に、ユーザーを判定するのに利用する。reset_password_sent_at パスワードリセットメール送信時刻。
パスワードリセットメールの有効期限の判定に利用する。設定
config/initializers/devise.rb# パスワードリセット時にキーになるカラム。 config.reset_password_keys = [:email] # パスワードリセットの有効期限。 config.reset_password_within = 6.hours # パスワードリセット後に自動ログイン。 config.sign_in_after_reset_password = trueメソッド
# パスワードリセットメール送信 user.send_reset_password_instructions # パスワードリセット # user.reset_password(new_password, new_password_confirmation) user.reset_password('password123', 'password123') # reset_password_tokenが有効期限内かどうかを、reset_password_sent_atを使い判定 user.reset_password_period_valid? #=> true # tokenを使ってuserを取得 User.with_reset_password_token(token) #=> userメール
メーラー#メソッド 概要 Devise::Mailer#reset_password_instructions パスワードリセットメール 参考
008 Validatableモジュール
? Validatableモジュールはemailとpasswordのバリデーションを提供するよ。Validatableモジュールを利用すると、サインアップで不正なemailとpasswordを入力した際にバリデーションエラーを表示してくれるようになるよ。
バリデーション項目
? emailに対しては以下の3つのバリデーションを設定するよ。
# emailが存在すること validates_presence_of :email, if: :email_required? # emailがユニークであること validates_uniqueness_of :email, allow_blank: true, if: :email_changed? # emailが正規表現にマッチすること # デフォルトのemail正規表現は`/\A[^@\s]+@[^@\s]+\z/` validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed?? passwordに対しては以下の3つのバリデーションを設定するよ。
# passwordが存在すること validates_presence_of :password, if: :password_required? # passwordとpassword_confirmationが合致すること validates_confirmation_of :password, if: :password_required? # passwordが指定文字数以内であること # デフォルトは6文字から128文字 validates_length_of :password, within: password_length, allow_blank: true設定
config/initializers/devise.rb# passwordの長さ。 # Rangeで指定。この場合は6文字から128文字。 config.password_length = 6..128 # emailバリデーションで利用する正規表現 config.email_regexp = /\A[^@\s]+@[^@\s]+\z/参考
009 Confirmableモジュール
? Confirmableモジュールはサインアップ時に本登録用のメールを送信して、登録されたメールアドレスが実際にユーザーのものであるか確認する機能を提供するよ。サインアップ時に仮登録になって、メール内のリンクをクリックすると本登録になる、よくあるやつだね。Confirmableモジュールを使わない場合は、emailとpasswordでユーザー登録した時点で本登録になるよ。
導入
? Confirmableモジュールはデフォルトで無効になっているので、有効にしていくよ。
? UserモデルでConfirmableモジュールを有効にするよ。
app/models/user.rbclass User < ApplicationRecord # :confirmableを追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable end? Confirmableモジュールで必要となるカラムを追加するよ。
$ rails g migration add_confirmable_columns_to_users invoke active_record create db/migrate/20201115225427_add_confirmable_columns_to_users.rbdb/migrate/20201115225427_add_confirmable_columns_to_users.rbclass AddConfirmableColumnsToUsers < ActiveRecord::Migration[6.0] def change change_table :users do |t| # Confirmableに必要なカラム t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.string :unconfirmed_email end add_index :users, :confirmation_token, unique: true end end$ rails db:migrate? これで完了だよ。
? 実際に試してみるね。設定を反映させるためにサーバーを再起動してね。まずはサインアップするよ。
? サインアップすると『メールを送ったので、リンクをクリックしてアカウントをアクティベートしてね』的なメッセージが表示され、同時にConfirm指示メールが送信されるよ。
? ちなみにこの時点ではまだユーザーは仮登録のような状態なのでログインすることはできないよ。ログインしようとするとこんなエラーメッセージが表示されるよ。
? Confirm指示メールの『Confirm my account』リンクをクリックすると、アカウントがConfirmされてログインできるようになるよ。
コントローラーとルーティング
? Confirmableモジュールでは
Devise::ConfirmationsController
というコントローラーと以下の3つのアクションが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/confirmation devise/confirmations#show confirm。
メールのリンク先はここ。
クエリパラメーターのconfirmation_tokenが一致しないとアクセスできない。GET /users/confirmation/new devise/confirmations#new confirm指示メール再送信画面。 POST /users/confirmation devise/confirmations#create confirm指示メール送信。 ?
/users/confirmation/new
はconfirm指示再送信画面で、ここからconfirm指示メールを再送信できるよ。参考
- app/controllers/devise/confirmations_controller.rb
カラム
? Confirmableモジュールでは、usersテーブルに以下の4つのカラムが必要になるよ。
カラム 概要 confirmation_token confirmする際に利用するトークン。
一意のランダムなトークンが生成される。
confirm指示メールからconfirmアクション(/users/confirmattion
)へアクセスする際に、ユーザーを判定するのに利用する。confirmed_at confirmされた時刻。
confirm済みかどうかはこのカラムがnilかどうかで判定する。confirmation_sent_at confirmation_token作成時刻。 unconfirmed_email まだconfirmされていないメールアドレス。
email変更時のconfirmで利用する。config.unconfirmed_email = true
の場合だけ必要。
confirmされるまでは新しいはemailはこのカラムに保存され、confirm時にemailのカラムにコピーされる。設定
config/initializers/devise.rb# confirmなしでログインできる期間。 # これを設定すると一定期間はconfirm前でもログインできるようになる。 # nilに設定すると無期限にログインできるようになる。 # デフォルトは 0.days。(confirmなしにはログインできない。) config.allow_unconfirmed_access_for = 2.days # confirmation_tokenの有効期限。 # ユーザーはこの期限内にconfirm指示メールのリンクをクリックしないといけない。 # デフォルトは nil。(制限なし。) config.confirm_within = 3.days # サインアップ時だけでなく、email変更時にもConfirmメールを送信する。 # unconfirmed_emailカラムが必要。 config.reconfirmable = true # confirmのキー。 config.confirmation_keys = [:email]メソッド
# confirmする # 具体的にはconfirmed_atに現在時刻を設定する user.confirm # confirm済みなら、true user.confirmed? # 手動でConfirmメールを送信 user.send_confirmation_instructionsメール
メーラー#メソッド 概要 Devise::Mailer#confirmation_instructions confirm指示メール 参考
010 Trackableモジュール
? Trackableモジュールはログイン時にIPアドレス・ログイン時刻・ログイン回数をDBに保存する機能を提供するよ。データはただ保存するだけで、Devise内部で使うわけではないよ。
導入
? Trackableモジュールはデフォルトで無効になっているので、有効にしていくよ。
? まずはUserモデルでTrackableモジュールを有効にするよ。
app/models/user.rbclass User < ApplicationRecord # :trackableを追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :trackable end? Trackableモジュールで必要となるカラムを追加するよ。
$ rails g migration add_trackable_columns_to_users invoke active_record create db/migrate/20201115004935_add_trackable_columns_to_users.rbdb/migrate/20201115004935_add_trackable_columns_to_users.rbclass AddTrackableColumnsToUsers < ActiveRecord::Migration[6.0] def change change_table :users do |t| # 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 end end end$ rails db:migrate? これで完了だよ。サーバーを再起動してユーザーがログインすると、追加したカラムに自動的にログイン情報が保存されるよ。
user.sign_in_count #=> 1 user.current_sign_in_at #=> Sun, 15 Nov 2020 00:55:35 UTC +00:00カラム
? Trackableモジュールでは、usersテーブルに以下の5つのカラムが必要になるよ。ログイン時にこれらのカラムにデータが保存されるよ。
カラム 概要 sign_in_count ログイン回数 current_sign_in_at 最新のログイン時刻 last_sign_in_at 1つ前のログイン時刻 current_sign_in_ip 最新のログイン時IPアドレス last_sign_in_ip 1つ前のログイン時IPアドレス 011 Timeoutableモジュール
? Timeoutableモジュールは一定期間アクセスがないと強制ログアウトさせる機能を提供するよ。
導入
? Timeoutableモジュールはデフォルトで無効になっているので、有効にしていくよ。
? UserモデルでTimeoutableモジュールを有効にするよ。
app/models/user.rbclass User < ApplicationRecord # :timeoutableを追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :timeoutable end? 次にタイムアウト時間の設定を行うよ。今回は動作確認のために10秒でタイムアウトになるように設定するね。
config/initializers/devise.rb- #config.timeout_in = 30.minutes + config.timeout_in = 10.seconds? これで完了だよ。
? 実際に試してみるね。設定を反映させるためにサーバーを再起動してね。ユーザーがログインしてから10秒間何もせずに放置してからアクセスすると、強制ログアウトになりログイン画面にリダイレクトされるよ。
? Timeoutableモジュールは、ログイン後にユーザーがアクセスする度に、sessionにアクセス時刻を保存しているんだ。そして前回のリクエスト時刻と今回のリクエスト時刻を比較して、
config.timeout_in
(タイムアウト時間)を超えている場合にログアウトさせているよ。設定
config/initializers/devise.rb# タイムアウト時間 config.timeout_in = 30.minutesメソッド
# タイムアウトならtrue user.timedout?(Time.current)参考
012 Lockableモジュール
? Lockableモジュールはログイン時に指定回数パスワードを間違えるとアカウントをロックする機能を提供するよ。
導入
? Lockableモジュールはデフォルトで無効になっているので、有効にしていくよ。
? UserモデルでLockableモジュールを有効にするよ。
app/models/user.rbclass User < ApplicationRecord # :lockableを追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :lockable end? Lockableモジュールで必要となるカラムを追加するよ。
$ rails g migration add_lockable_columns_to_users invoke active_record create db/migrate/20201115111752_add_lockable_columns_to_users.rbdb/migrate/20201115111752_add_lockable_columns_to_users.rbclass AddTrackableColumnsToUsers < ActiveRecord::Migration[6.0] def change change_table :users do |t| # Lockableに必要なカラム t.integer :failed_attempts, default: 0, null: false t.string :unlock_token t.datetime :locked_at end add_index :users, :unlock_token, unique: true end end$ rails db:migrate? 次にログインの上限試行回数の設定を行うよ。今回は動作確認のために2回ログインに失敗したらロックするように設定するよ。
config/initializers/devise.rb- # config.maximum_attempts = 20 + config.maximum_attempts = 2? これで完了だよ。
? 実際に試してみるね。設定を反映させるためにサーバーを再起動してね。ログインで2回パスワードを間違えると『アカウントがロックされました。』的なエラーメッセージが表示されて、これ以降は正しいパスワードを入力してもログインできなくなるよ。
? アカウントロック時には、ユーザーに以下のようなアンロック指示メールが送信されるよ。
? ユーザーはこのメールの『Unlock my account』リンクをクリックすると、アカウントがアンロックされて、再びログインできるようになるよ。
メールではなく一定時間経過でアンロックさせる
? Lockableモジュールではメールでアンロックする方法以外に、一定時間経過でアンロックする方法も提供しているよ。以下の設定を行うとメールではなく時間経過でアンロックできるよ。
config/initializers/devise.rb- # config.unlock_strategy = :email + config.lock_strategy = :time? 次にアンロックまでの時間の設定を行うよ。今回は動作確認のために10秒でアンロックするように設定するよ。
config/initializers/devise.rb- # config.unlock_in = 1.hour + config.unlock_in = 10.seconds? これで完了だよ。
? 実際に試してみるね。設定を反映させるためにサーバーを再起動してね。ログインで2回パスワードを間違えるとアカウントがロックされるよ。その後10秒待つと、アカウントが自動でアンロックされて、再びログインできるようになるよ。
コントローラーとルーティング
? Lockableモジュールでは
Devise::UnlocksController
というコントローラーと以下の3つのアクションが用意されるよ。
HTTPメソッド path コントローラー#アクション 目的 GET /users/unlock devise/unlocks#show アンロック。
メールのリンク先はここ。
クエリパラメーターのunlock_tokenが一致しないとアクセスできない。GET /users/unlock/new devise/unlocks#new アンロック指示メール再送信画面。 POST /users/unlock devise/unlocks#create アンロック指示メール送信。 ?
/users/unlock/new
はアンロック指示メール再送信画面で、ここからアンロック指示メールを再送信できるよ。カラム
? Lockableモジュールでは、usersテーブルに以下の3つのカラムが必要になるよ。
カラム 概要 failed_attempts 失敗回数。 config.lock_strategy
が:failed_attempts
の場合にだけ必要。unlock_token メールからアンロックする際に利用するtoken。
一意のランダムなトークンが生成される。
アンロック指示メールからアンロックアクション(/users/unlock
)へアクセスする際に、ユーザーを判定するのに利用する。config.unlock_strategy
が:both
の場合にだけ必要。locked_at ロック時刻。
これがnullでない場合にロック状態とみなされる。設定
config/initializers/devise.rb# ロック方法 # - failed_attempts: 指定回数間違えたらロック # - none: 自動ロックはなしで、サーバ管理者が手動でロック config.lock_strategy = :failed_attempts # アンロックのキー config.unlock_keys = [:email] # アンロック方法 # - email: メールでアンロックのリンクを送信(config.maximum_attemptsと一緒に使う) # - time: 数時間後にアンロック(config.unlock_inと一緒に使う) # - both: emailとtimeの両方 # - none: 自動アンロックはなしで、サーバ管理者が手動でアンロック config.unlock_strategy = :both # ロックまでの回数 config.maximum_attempts = 20 # アンロックまでの時間(`config.unlock_strategy = :time`の場合) config.unlock_in = 1.hour # ロック前に警告する config.last_attempt_warning = trueメソッド
# ロック(メール送信もする) user.lock_access! # ロック(メール送信しない) user.lock_access!(send_instructions: false) # アンロック user.unlock_access! # アンロックのメール送信 user.resend_unlock_instructionsメール
メーラー#メソッド 概要 Devise::Mailer#unlock_instructions アカウントアンロック指示メール 参考
013 Omniauthableモジュール
? OmniauthableモジュールはDeviseとOmniAuth gemとの連携機能を提供するよ。Omniauthableモジュールを使うことで、ユーザーはTwitterアカウントやGoogleアカウントなどでログインできるようになるよ。
OmniAuthとは?
?? OmniAuthってなに?
? すごくざっくりいうと、OAuthを利用して、TwitterやGoogleのアカウントでアプリケーションにログインできるようにするgemだよ。もう少しちゃんと説明すると、OmniAuthは複数プロバイダーを介した認証を標準化するgemだよ。OmniAuthはStrategyという仕組みを提供することで、別個の認証を共通のインターフェースで認証できるようになるんだ。例えばTwitterアカウントとGoogleアカウントでログインできるようなアプリケーションを考えてみてね。このときTwitterとGoogleでプロバイダーが異なるんだけど、Twitterに対応するStrategyとGoogleに対応するStrategyを用意すれば、OmniAuthを介して同じインターフェースで認証ができるようになるよ。
? Strategyは自分で用意することもできるけど、主要なプロバイダーに対応するStrategyは既にgemとして用意されているから、それを使えばOKだよ( List of Strategies · omniauth/omniauth Wiki · GitHub )。これらのStrategyのgemはブラックボックスとして利用することができて、OAuthのような複雑なフローを自分で実装することなく、簡単にOAuthを実現できるようになっているよ。StrategyはRackミドルウェアとして実装されて、
omniauth-<プロバイダー名>
のような名称のgemとして提供されるよ。? OmniAuthをOmniauthableモジュール経由で使う場合は、
omniauth-twitter
やomniauth-google-oauth2
などのOAuthを利用したログインを実装することがほとんどだよ。ただOmniAuth自体はOAuthだけでなくemail/passwordによる認証やBasic認証なんかもStrategyとして利用できるようになっているよ。? Twitterであれ、Googleであれ、OmniAuthの使い方はだいたい同じだよ。ただプロバイダーから取得できるデータ(emailを取得できたりできなかったり)やAPI keyの取得方法など、細かい点は変わってくるよ。
参考
- GitHub - omniauth/omniauth: OmniAuth is a flexible authentication system utilizing Rack middleware.
- OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと - Qiita
OmniAuth Twitter - Twitterアカウントによるログイン
?
omniauth-twitter
gemを使えばTwitterアカウントでログインできるようになるよ。詳しくは以下の記事を参考にしてね。
- GitHub - arunagw/omniauth-twitter: OmniAuth strategy for Twitter
- [Rails] deviseの使い方(rails6版) - Qiita
- [Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する - Qiita
OmniAuth Google OAuth2 - Googleアカウントによるログイン
?
omniauth-google-oauth2
gemを使えばGoogleアカウントでログインできるようになるよ。詳しくは以下の記事を参考にしてね。
- GitHub - zquestz/omniauth-google-oauth2: Oauth2 strategy for Google
- 爆速ッ!! gem omniauth-google-oauth2 で認証させる - Qiita
- [Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する - Qiita
OmniAuth Facebook - Facebookアカウントによるログイン
?
omniauth-facebook
gemを使えばFacebookアカウントでログインできるようになるよ。詳しくは以下の記事を参考にしてね。
- GitHub - simi/omniauth-facebook: Facebook OAuth2 Strategy for OmniAuth
- [Devise How-To] OmniAuth: 概要(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
- railsでomniauth-facebookを使ってFacebookログインを実装する方法 - Qiita
第3章 ビューをカスタマイズする
014 ビューをカスタマイズする
? Deviseで利用されるビューファイルの優先順位は以下のようになってるよ。
- アプリ内のdeviseビュー(
devise/sessions/new.html.erb
)- gem内のdeviseビュー(
devise/sessions/new.html.erb
)? デフォルトではビューファイルはgemの中にあって、それを使うようになっているんだ。なのでビューをカスタマイズしたい場合は、gemの中のビューファイルを自分のアプリにコピーしてから、それを自分で修正していけばOKだよ。
? まずは自分のアプリにビューファイルをコピーしてね。以下のコマンド打てばビューファイルがコピーされるよ。
$ rails g devise:views invoke Devise::Generators::SharedViewsGenerator exist app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for exist app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb exist app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb exist app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb exist app/views/devise/sessions create app/views/devise/sessions/new.html.erb exist app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb exist app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb? あとは作成されたビューを自分で修正していけばOKだよ。
? ちなみに、UserとAdminのように複数モデルを利用するのでなければ、わざわざScope指定する必要はないよ。
# こんな感じでusers Scopeを指定して作成することも可能 # ただconfig.scoped_viewsを設定したりカスタムコントローラーが必須だったり色々面倒。 # 複数モデルを利用するのでなければ、わざわざScope指定する必要はないよ。 $ rails g devise:views users invoke Devise::Generators::SharedViewsGenerator create app/views/users/shared create app/views/users/shared/_error_messages.html.erb create app/views/users/shared/_links.html.erb invoke form_for create app/views/users/confirmations create app/views/users/confirmations/new.html.erb create app/views/users/passwords create app/views/users/passwords/edit.html.erb create app/views/users/passwords/new.html.erb create app/views/users/registrations create app/views/users/registrations/edit.html.erb create app/views/users/registrations/new.html.erb create app/views/users/sessions create app/views/users/sessions/new.html.erb create app/views/users/unlocks create app/views/users/unlocks/new.html.erb invoke erb create app/views/users/mailer create app/views/users/mailer/confirmation_instructions.html.erb create app/views/users/mailer/email_changed.html.erb create app/views/users/mailer/password_change.html.erb create app/views/users/mailer/reset_password_instructions.html.erb create app/views/users/mailer/unlock_instructions.html.erb015 レイアウトテンプレートをカスタマイズする
Devise全体のレイアウトテンプレートを用意する場合
? Deviseはデフォルトでは通常のビューと同じくレイアウトテンプレートに
application.html.erb
を利用するよ。Deviseのレイアウトテンプレートを変更したい場合はapp/views/layouts/devise.html.erb
を用意すれば自動でそっちを使ってくれるので、app/views/layouts/devise.html.erb
を用意すればOKだよ。コントローラー毎・アクション毎にレイアウトテンプレートを用意する場合
? コントローラー毎・アクション毎にレイアウトテンプレートを用意したい場合は、各コントローラーで
layout
メソッドを利用するよ。? まずジェネレーターを利用してカスタムコントローラーを作成してね。
$ rails g devise:controllers users create app/controllers/users/confirmations_controller.rb create app/controllers/users/passwords_controller.rb create app/controllers/users/registrations_controller.rb create app/controllers/users/sessions_controller.rb create app/controllers/users/unlocks_controller.rb create app/controllers/users/omniauth_callbacks_controller.rb? コントローラーで
layout
メソッドを使ってレイアウトテンプレートを指定してね。app/controllers/users/registerations_controller.rbclass User::RegistrationsController < Devise::RegistrationsController # RegistrationsControllerのeditアクションではyour_layoutファイルを使うようにする layout "your_layout", only: [:edit] end? 別の方法として、カスタムコントローラーを作成せずに設定ファイルで指定することも可能だよ。
config/application.rbconfig.to_prepare do # コントローラー毎にレイアウトファイルを指定できる Devise::SessionsController.layout "your_layout" Devise::RegistrationsController.layout "your_layout" Devise::ConfirmationsController.layout "your_layout" Devise::UnlocksController.layout "your_layout" Devise::PasswordsController.layout "your_layout" end016 バリデーションエラーの表示をカスタマイズする
? Deviseではバリデーションエラーの表示もパーシャルとしてデフォルトで用意されているよ。
? 例えばサインアップ画面ではこんな感じでバリデーションエラー表示のパーシャルをrenderしているよ。
app/views/devise/registrations/new.html.erb<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <!-- ここ --> <%= render "devise/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <div class="field"> <%= f.label :password %> <% if @minimum_password_length %> <em>(<%= @minimum_password_length %> characters minimum)</em> <% end %><br /> <%= f.password_field :password, autocomplete: "new-password" %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "new-password" %> </div> <div class="actions"> <%= f.submit "Sign up" %> </div> <% end %> <%= render "devise/shared/links" %>? バリデーションエラー表示のパーシャルでは
resource
のエラーを表示しているよ。resourceはUserインスタンスだよ。app/views/devise/shared/_error_messages.html.erb<!-- resource(Userインスタンス)にバリデーションエラーがあれば、エラー内容を表示 --> <% if resource.errors.any? %> <div id="error_explanation"> <h2> <%= I18n.t("errors.messages.not_saved", count: resource.errors.count, resource: resource.class.model_name.human.downcase) %> </h2> <ul> <% resource.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %>? バリデーションエラーの表示をカスタマイズしたい場合は、この
_error_messages.html.erb
をカスタマイズすればOKだよ。? ちなみに古いバージョンだと
_error_messages.html.erb
を利用せずに、devise_error_messages!
というメソッドを利用している場合があるよ。その場合は_error_messages.html.erb
を自分で用意するか、devise_error_messages!
をオーバーライドすることでカスタマイズできるよ。詳しくはこちらを参考にしてね -> Override devise_error_messages! for views · heartcombo/devise Wiki · GitHub参考
017 ビューをHamlにする
? まずは自分のアプリにビューファイルをコピーしてね。
$ rails g devise:views invoke Devise::Generators::SharedViewsGenerator exist app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for exist app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb exist app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb exist app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb exist app/views/devise/sessions create app/views/devise/sessions/new.html.erb exist app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb exist app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb? ErbファイルをHamlファイルに変換するために、
html2haml
というツールを利用するよ。html2haml
をインストールしてね。一度しか使わないのでgem
コマンドでインストールしちゃってOKだよ。$ gem install html2haml? 全てのErbファイルをHamlに変換するよ。
# 全ErbファイルをHamlファイルに変換 $ find ./app/views/devise -name \*.erb -print | sed 'p;s/.erb$/.haml/' | xargs -n2 html2haml # 全Erbファイルを削除 $ rm app/views/devise/**/*.erb参考
018 ビューをSlimにする
? まずは自分のアプリにビューファイルをコピーしてね。
$ rails g devise:views invoke Devise::Generators::SharedViewsGenerator exist app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for exist app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb exist app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb exist app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb exist app/views/devise/sessions create app/views/devise/sessions/new.html.erb exist app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb exist app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb? ErbファイルをSlimファイルに変換するために、
html2slim
というツールを利用するよ。html2slim
をインストールしてね。一度しか使わないのでgem
コマンドでインストールしちゃってOKだよ。$ gem install html2slim? 全てのErbファイルをSlimに変換するよ。
# 全ErbファイルをSlimファイルに変換 $ find ./app/views/devise -name \*.erb -print | sed 'p;s/.erb$/.slim/' | xargs -n2 html2slim # 全Erbファイルを削除 $ rm app/views/devise/**/*.erb019 Bootstrap4用のビューを利用する
?
devise-bootstrap-views
というgemを使うとBootstrap用のビューをgenerateできるようになるよ。? 日本語を使いたい場合は、
devise-i18n
というI18n対応のDeviseビューを作成するgemも一緒に入れるといいよ。Gemfilegem 'devise-i18n' gem 'devise-bootstrap-views'$ bundle install? ビューのジェネレーターでBootstrapテンプレートを指定してね。
$ rails g devise:views:bootstrap_templates create app/views/devise create app/views/devise/confirmations/new.html.erb create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb create app/views/devise/sessions/new.html.erb create app/views/devise/shared/_links.html.erb create app/views/devise/unlocks/new.html.erb? こんな感じでBootstrapのクラスを利用したビューファイルが作成されるよ。
app/views/devise/sessions/new.html.erb<h1><%= t('.sign_in') %></h1> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <!-- form-groupとかのBootstrap用のclassが付与されている。--> <div class="form-group"> <%= f.label :email %> <%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'form-control' %> </div> <div class="form-group"> <%= f.label :password %> <%= f.password_field :password, autocomplete: 'current-password', class: 'form-control' %> </div> <% if devise_mapping.rememberable? %> <div class="form-group form-check"> <%= f.check_box :remember_me, class: 'form-check-input' %> <%= f.label :remember_me, class: 'form-check-label' do %> <%= resource.class.human_attribute_name('remember_me') %> <% end %> </div> <% end %> <div class="form-group"> <%= f.submit t('.sign_in'), class: 'btn btn-primary' %> </div> <% end %> <%= render 'devise/shared/links' %>? ログイン画面の見た目がBootstrapになってるよ。
?
devise-bootstrap-views
はあくまでBootstrap4用のビューを用意してくれるだけだよ。Bootstrap4自体は自分でセットアップする必要があるので注意してね。参考
第4章 コントローラーをカスタマイズする
020 コントローラーをカスタマイズする
? コントローラーをカスタマイズするためには、ジェネレーターを利用してコントローラーを生成する必要があるよ。
$ rails g devise:controllers users create app/controllers/users/confirmations_controller.rb create app/controllers/users/passwords_controller.rb create app/controllers/users/registrations_controller.rb create app/controllers/users/sessions_controller.rb create app/controllers/users/unlocks_controller.rb create app/controllers/users/omniauth_callbacks_controller.rb =============================================================================== Some setup you must do manually if you haven't yet: Ensure you have overridden routes for generated controllers in your routes.rb. For example: Rails.application.routes.draw do devise_for :users, controllers: { sessions: 'users/sessions' } end ===============================================================================? 生成されたコントローラーはこんな感じだよ。Deviseのコントローラーのサブクラスになっているよ。ビューの生成ではgem内のビューをそのままコピーするのに対して、コントローラーの生成ではgem内のコントローラーを継承したクラスを生成するよ。ビューの生成とはちょっと違うので注意してね。
app/controllers/users/sessions_controller.rb# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController # before_action :configure_sign_in_params, only: [:create] # GET /resource/sign_in # def new # super # end # POST /resource/sign_in # def create # super # end # DELETE /resource/sign_out # def destroy # super # end # protected # If you have extra params to permit, append them to the sanitizer. # def configure_sign_in_params # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) # end end? ルーティングも変更する必要があるよ。デフォルトだとこんな感じでDevise gem内の
devise
名前空間のコントローラーを利用するようになっているよ。config/routes.rbdevise_for :users$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel new_user_registration GET /users/sign_up(.:format) devise/registrations#new edit_user_registration GET /users/edit(.:format) devise/registrations#edit user_registration PATCH /users(.:format) devise/registrations#update PUT /users(.:format) devise/registrations#update DELETE /users(.:format) devise/registrations#destroy POST /users(.:format) devise/registrations#create? さっき生成したアプリ内の
users
名前空間のコントローラーを利用するように変更するよ。コントローラー毎に指定する必要があるので、カスタマイズしたいコントローラーだけ指定してね。config/routes.rb# 利用するモジュールのコントローラーを指定する # 今回はデフォルトで有効なpasswords、registrations、sessionsの3つを指定 devise_for :users, controllers: { passwords: 'users/passwords', registrations: 'users/registrations', sessions: 'users/sessions', # confirmations: 'users/confirmations', # unlocks: 'users/unlocks', # omniauth_callbacks: 'users/omniauth_callbacks', }$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) users/sessions#new user_session POST /users/sign_in(.:format) users/sessions#create destroy_user_session DELETE /users/sign_out(.:format) users/sessions#destroy new_user_password GET /users/password/new(.:format) users/passwords#new edit_user_password GET /users/password/edit(.:format) users/passwords#edit user_password PATCH /users/password(.:format) users/passwords#update PUT /users/password(.:format) users/passwords#update POST /users/password(.:format) users/passwords#create cancel_user_registration GET /users/cancel(.:format) users/registrations#cancel new_user_registration GET /users/sign_up(.:format) users/registrations#new edit_user_registration GET /users/edit(.:format) users/registrations#edit user_registration PATCH /users(.:format) users/registrations#update PUT /users(.:format) users/registrations#update DELETE /users(.:format) users/registrations#destroy POST /users(.:format) users/registrations#create? あとは生成したコントローラーを好きなようにカスタマイズすればOKだよ。
app/controllers/users/sessions_controller.rb# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController # before_action :configure_sign_in_params, only: [:create] # GET /resource/sign_in def new # 自由にカスタマイズする # コメントアウトされたアクションについては、Devise::SessionsControllerのアクションがそのまま使われるので挙動は変わらないよ logger.debug params super end # POST /resource/sign_in # def create # super # end # DELETE /resource/sign_out # def destroy # super # end # protected # If you have extra params to permit, append them to the sanitizer. # def configure_sign_in_params # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) # end end?
super
は親クラスであるDevise::SessionsController
のメソッド呼び出しだよ。super
部分の挙動を変えたい場合はDevise::SessionsController
のコードを見ながら変更してね。Devise::SessionsController
のコードを見るにはDevise本体のコードを見る必要があるよ。gem内のapp/controllers/devise/
配下に置かれているから探してみてね。# https://github.com/heartcombo/devise/blob/master/app/controllers/devise/sessions_controller.rb # frozen_string_literal: true class Devise::SessionsController < DeviseController prepend_before_action :require_no_authentication, only: [:new, :create] prepend_before_action :allow_params_authentication!, only: :create prepend_before_action :verify_signed_out_user, only: :destroy prepend_before_action(only: [:create, :destroy]) { request.env["devise.skip_timeout"] = true } # GET /resource/sign_in def new self.resource = resource_class.new(sign_in_params) clean_up_passwords(resource) yield resource if block_given? respond_with(resource, serialize_options(resource)) end # POST /resource/sign_in def create self.resource = warden.authenticate!(auth_options) set_flash_message!(:notice, :signed_in) sign_in(resource_name, resource) yield resource if block_given? respond_with resource, location: after_sign_in_path_for(resource) end # DELETE /resource/sign_out def destroy signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)) set_flash_message! :notice, :signed_out if signed_out yield if block_given? respond_to_on_destroy end protected def sign_in_params devise_parameter_sanitizer.sanitize(:sign_in) end def serialize_options(resource) methods = resource_class.authentication_keys.dup methods = methods.keys if methods.is_a?(Hash) methods << :password if resource.respond_to?(:password) { methods: methods, only: [:password] } end def auth_options { scope: resource_name, recall: "#{controller_path}#new" } end def translation_scope 'devise.sessions' end private # Check if there is no signed in user before doing the sign out. # # If there is no signed in user, it will set the flash message and redirect # to the after_sign_out path. def verify_signed_out_user if all_signed_out? set_flash_message! :notice, :already_signed_out respond_to_on_destroy end end def all_signed_out? users = Devise.mappings.keys.map { |s| warden.user(scope: s, run_callbacks: false) } users.all?(&:blank?) end def respond_to_on_destroy # We actually need to hardcode this as Rails default responder doesn't # support returning empty response on GET request respond_to do |format| format.all { head :no_content } format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) } end end end?
Devise::SessionsController
のコードをコピペすればそのまま動くので、好きなようにカスタマイズしてね。app/controllers/users/sessions_controller.rb# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController # before_action :configure_sign_in_params, only: [:create] # GET /resource/sign_in def new self.resource = resource_class.new(sign_in_params) clean_up_passwords(resource) # 自由にカスタマイズする logger.debug resource.attributes yield resource if block_given? respond_with(resource, serialize_options(resource)) end # POST /resource/sign_in # def create # super # end # DELETE /resource/sign_out # def destroy # super # end # protected # If you have extra params to permit, append them to the sanitizer. # def configure_sign_in_params # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) # end end参考
021 Strong Parameterをカスタマイズする
? ログインフォームやサインアップフォームにテキストフィールドを追加したい場合があるよね。でもDeviseではStrong Parameterで許可される属性がデフォルトで決まっているため、ビューだけでなくStrong Parameterも変更する必要があるんだ。
? デフォルトで許可されている属性は以下の通りだよ。
コントローラー#アクション 識別子 概要 許可されている属性 devise/sessions#create :sign_in ログイン devise/registrations#create :sign_up サインアップ email, password, pasword_confirmation devise/registrations#update :account_update ユーザー更新 email, password_confirmation, current_password ? 例えばサインアップ画面で
password
、pasword_confirmation
に加えて、username
も入力させたいとする。そんな場合はStrong Parameterでusername
も追加で許可する必要がある。以下のようにdevise_parameter_sanitizer.permit
を利用すればOKだよ。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base # devise_controller?はDeviseコントローラーの場合だけtrueを返す # つまりconfigure_permitted_parametersはDeviseコントローラーの場合だけ実行される before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters # サインアップ時にusernameも追加で許可する devise_parameter_sanitizer.permit(:sign_up, keys: [:username]) end end?
devise_parameter_sanitizer
の使い方は以下の通りだよ。# keysオプションを使うと、permitする属性を追加できる # デフォルトでpermitされているpassword/password_confirmationに加えて、usernameもpermitする devise_parameter_sanitizer.permit(:sign_up, keys: [:username]) # exceptオプションを使うと、permitしない属性を指定できる # passwordだけpermitする devise_parameter_sanitizer.permit(:sign_up, except: [:password_confirmation]) # ブロックを使うと完全にオーバーライドできる # email, password, password_confirmationをpermitする devise_parameter_sanitizer.permit(:sign_up) do |user| user.permit(:email, :password, :password_confirmation) end # accepts_nested_attributes_forを利用している場合は、xxxx_attributesを使うと関連先の属性もpermitできる devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, address_attributes: [:country, :state, :city, :area, :postal_code]])? サインアップとユーザー更新の2つの挙動を変更したい場合は、2回
devise_parameter_sanitizer.permit
を使ってね。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters # サインアップとユーザー更新の2つの挙動を変更 devise_parameter_sanitizer.permit(:sign_up, keys: [:username] devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :phone, :email, bank_attributes: [:bank_name, :bank_account]]) end end参考
- GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.
- devise/parameter_sanitizer.rb at master · heartcombo/devise · GitHub
- devise - How to specify devise_parameter_sanitizer for edit action? - Stack Overflow
022 リダイレクト先を変更する
? デフォルトではログアウト時はroot_pathにリダイレクトされるようになっている。
ApplicationController
にafter_sign_out_path_for
メソッドを定義してpathを返すようにすれば、リダイレクト先を変更できるよ。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base private def after_sign_out_path_for(resource_or_scope) # デフォルトはroot_path new_user_session_path end end? リダイレクト先の変更には以下のメソッドを利用できるよ。使い方は
after_sign_out_path_for
と同じだよ。
メソッド名 概要 after_sign_out_path_for ログアウト時のリダイレクト先 after_sign_in_path_for サインイン時のリダイレクト先 after_sign_up_path_for サインアップ時のリダイレクト先 after_inactive_sign_up_path_for サインアップ時のリダイレクト先(Confirmableモジュール利用時) after_update_path_for ユーザー更新時のリダイレクト先 after_confirmation_path_for メール確認時のリダイレクト先 after_resending_confirmation_instructions_path_for 確認メール再送信時のリダイレクト先 after_omniauth_failure_path_for Omniauth失敗時のリダイレクト先 after_sending_reset_password_instructions_path_for パスワードリセット時のリダイレクト先 ? 複数モデル利用している場合は、引数の
resource_or_scope
を使うとリダイレクト先を分岐させられるよ。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base private def after_sign_out_path_for(resource_or_scope) if resource_or_scope == :user new_user_session_path elsif resource_or_scope == :admin new_admin_session_path else root_path end end end参考
- How To: Change the redirect path after destroying a session i.e. signing out · heartcombo/devise Wiki · GitHub
- devise/helpers.rb at master · heartcombo/devise · GitHub
- devise でアクションの完了後に移動するパスをカスタマイズしたい « yukku++
- Deviseでサインイン/サインアウト後のリダイレクト先を変更する - Qiita
- 【Devise】アカウント登録、ログイン/ ログアウト、アカウント編集後のリダイレクト先の変更 - Qiita
第5章 モデルをカスタマイズする
023 複数モデルを利用する
? DeviseではUser以外の別のモデルも認証対象のモデルとして扱えるよ。今回はUserとAdminという2つのモデルを使ってみるね。この2つのモデルはコントローラー・ビュー・ルーティングも全て別物として扱われるよ。
? 設定ファイルを作成するところからやるよ。
$ rails g devise:install? 設定ファイルを変更するよ。
config/initializers/devise.rb- # config.scoped_views = false + config.scoped_views = true? DeviseではScopeという機能を使ってUserとAdminという2つのモデルを使えるようにしているよ。(ActiveRecordのScopeとは別の機能。Scopeについて詳しく知りたい場合は 057 Warden を参照。)
scoped_views
をtrue
に設定すると、Scope用のビューを優先的に使うようになって、User用のビューとAdmin用のビューを別個に使えるようになるよ。scoped_viewsがfalseの場合のビューの優先順位(デフォルト)
- アプリ内のdeviseビュー(
devise/sessions/new.html.erb
)- gem内のdeviseビュー(
devise/sessions/new.html.erb
)scoped_viewsがtrueの場合のビューの優先順位
- Scope用のビュー(
users/sessions/new.html.erb
) # これを優先的に使うようにする- アプリ内のdeviseビュー(
devise/sessions/new.html.erb
)- gem内のdeviseビュー(
devise/sessions/new.html.erb
)? デフォルトでは高速化のため
scoped_views
はfalse
に設定されてるよ。? 次はUserモデルとAdminモデルを作成するよ。
$ rails g devise User $ rails g devise Admin? UserとAdminのコントローラーを作成してね。
$ rails g devise:controllers users $ rails g devise:controllers admins? UserとAdminのビューを作成してね。
$ rails g devise:views users $ rails g devise:views admins?
$ rails g devise:views
ではないので注意してね。Scope指定なしだとdevise
という名前空間でビューを作ってしまうよ。今回はconfig.scoped_views = true
に設定していて、UserとAdminにそれぞれ別のビューを用意するので、scopeまで指定してね。? ルーティングを設定するよ。まずは今のルーティングを確認してね。
config/routes.rbdevise_for :users devise_for :admins$ rails routes new_admin_session GET /admins/sign_in(.:format) devise/sessions#new admin_session POST /admins/sign_in(.:format) devise/sessions#create destroy_admin_session DELETE /admins/sign_out(.:format) devise/sessions#destroy new_admin_password GET /admins/password/new(.:format) devise/passwords#new edit_admin_password GET /admins/password/edit(.:format) devise/passwords#edit admin_password PATCH /admins/password(.:format) devise/passwords#update PUT /admins/password(.:format) devise/passwords#update POST /admins/password(.:format) devise/passwords#create ancel_admin_registration GET /admins/cancel(.:format) devise/registrations#cancel new_admin_registration GET /admins/sign_up(.:format) devise/registrations#new edit_admin_registration GET /admins/edit(.:format) devise/registrations#edit admin_registration PATCH /admins(.:format) devise/registrations#update PUT /admins(.:format) devise/registrations#update DELETE /admins(.:format) devise/registrations#destroy POST /admins(.:format) devise/registrations#create new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel new_user_registration GET /users/sign_up(.:format) devise/registrations#new edit_user_registration GET /users/edit(.:format) devise/registrations#edit user_registration PATCH /users(.:format) devise/registrations#update PUT /users(.:format) devise/registrations#update DELETE /users(.:format) devise/registrations#destroy POST /users(.:format) devise/registrations#create? UserとAdminがどちらも
devise
という名前空間のコントローラーを使ってしまっているよ。コントローラーもそれぞれ用意したいので、UserとAdminがそれぞれのコントローラーを利用するように修正するよ。config/routes.rbdevise_for :users, controllers: { # UserのSessionsControllerには、Users::SessionsControllerを利用する。他のコントローラーも同じように修正する。 sessions: 'users/sessions', passwords: 'users/passwords', registrations: 'users/registrations' } devise_for :admins, controllers: { # AdminのSessionsControllerには、Admins::SessionsControllerを利用する。他のコントローラーも同じように修正する。 sessions: 'admins/sessions', passwords: 'admins/passwords', registrations: 'admins/registrations' }? これでUserとAdminで別個のコントローラーを使えるよ。
$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) users/sessions#new user_session POST /users/sign_in(.:format) users/sessions#create destroy_user_session DELETE /users/sign_out(.:format) users/sessions#destroy new_user_password GET /users/password/new(.:format) users/passwords#new edit_user_password GET /users/password/edit(.:format) users/passwords#edit user_password PATCH /users/password(.:format) users/passwords#update PUT /users/password(.:format) users/passwords#update POST /users/password(.:format) users/passwords#create cancel_user_registration GET /users/cancel(.:format) users/registrations#cancel new_user_registration GET /users/sign_up(.:format) users/registrations#new edit_user_registration GET /users/edit(.:format) users/registrations#edit user_registration PATCH /users(.:format) users/registrations#update PUT /users(.:format) users/registrations#update DELETE /users(.:format) users/registrations#destroy POST /users(.:format) users/registrations#create new_admin_session GET /admins/sign_in(.:format) admins/sessions#new admin_session POST /admins/sign_in(.:format) admins/sessions#create destroy_admin_session DELETE /admins/sign_out(.:format) admins/sessions#destroy new_admin_password GET /admins/password/new(.:format) admins/passwords#new edit_admin_password GET /admins/password/edit(.:format) admins/passwords#edit admin_password PATCH /admins/password(.:format) admins/passwords#update PUT /admins/password(.:format) admins/passwords#update POST /admins/password(.:format) admins/passwords#create ancel_admin_registration GET /admins/cancel(.:format) admins/registrations#cancel new_admin_registration GET /admins/sign_up(.:format) admins/registrations#new edit_admin_registration GET /admins/edit(.:format) admins/registrations#edit admin_registration PATCH /admins(.:format) admins/registrations#update PUT /admins(.:format) admins/registrations#update DELETE /admins(.:format) admins/registrations#destroy POST /admins(.:format) admins/registrations#create? これで完了だよ。User用のサインアップページである
/users/sign_up
とは別に、Admin用の/admins/sign_up
にアクセスできるようになるよ。? あと補足として、UserとAdminのsessionは別々に管理されるため、
current_user
とは別にAdmin用のcurrent_admin
などのメソッドが用意されるよ。## User用 # ログイン中のuserを取得 current_user # userを認証 authenticate_user! # userがログイン済みならtrue user_signed_in? # userに紐づくsession user_session ## Admin用 # ログイン中のadminを取得 current_admin # adminを認証 authenticate_admin! # adminがログイン済みならtrue admin_signed_in? # adminに紐づくsession admin_session? またUserでのログイン状態とAdminでのログイン状態は別々に管理されるため、モデル毎にログイン/ログアウトが可能だよ。ただし以下の設定をすることでログアウト時に全モデルでログアウトさせるようにすることも可能だよ。
config/initializers/devise.rb# ログアウト時に全てのScopeでのログアウトとする。 # falseの場合は/users/sign_outでログアウトした場合、user Scopeだけでのログアウトになる。 config.sign_out_all_scopes = true? メッセージもモデル毎に指定可能だよ。
config/locales/devise.en.yml# 参照: https://github.com/heartcombo/devise/wiki/How-to-Setup-Multiple-Devise-User-Models#8-setting-custom-flash-messages-per-resource en: devise: confirmations: # User用の文言 confirmed: "Your email address has been successfully confirmed." # Admin用の文言 admin_user: confirmed: "Your admin email address has been successfully confirmed."参考
- How to Setup Multiple Devise User Models · heartcombo/devise Wiki · GitHub
- Railsでdeviseひとつで複数モデルを管理しよう - Qiita
024 emailの代わりにusernameでログインさせる
?? デフォルトではログインする際には
password
を入力するよね。??
username
を使ってログインしてもらうにはどうすればいいかな?? まずはusersテーブルに
username
カラムを追加してね。username
カラムは$ rails g migration add_username_to_users username:string:uniq invoke active_record create db/migrate/20201114030246_add_username_to_users.rb? マイグレーションファイルはこんな感じだよ。
db/migrate/20201114030246_add_username_to_users.rbclass AddUsernameToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :username, :string add_index :users, :username, unique: true end end$ rails db:migrate? usernameのバリデーションを設定してね。
app/models/user.rbvalidates :username, uniqueness: true, presence: true? 設定ファイルで認証キーを
username
に変更するよ。config/initializers/devise.rb- # config.authentication_keys = [:email] + config.authentication_keys = [:username]? サインアップ画面で
username
も入力できるように修正するよ。app/views/devise/registrations/new.html.erb<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <!-- usernameの入力欄を追加 --> + <div class="field"> + <%= f.label :username %><br /> + <%= f.text_field :username, autocomplete: "username" %> + </div> <div class="field"> <%= f.label :password %> <% if @minimum_password_length %> <em>(<%= @minimum_password_length %> characters minimum)</em> <% end %><br /> <%= f.password_field :password, autocomplete: "new-password" %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "new-password" %> </div> <div class="actions"> <%= f.submit "Sign up" %> </div> <% end %> <%= render "devise/shared/links" %>? 画面はこんな感じになるよ。
? Strong Parameterを設定するよ。このままだと認証キーでない
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters # サインアップ時にemail属性を許可する devise_parameter_sanitizer.permit(:sign_up, keys: [:email]) end end? ここまででサインアップができるようになったよ。次は
username
でログインできるようにするために、ログイン画面でusername
を使うように修正するよ。app/views/devise/sessions/new.html.erb<h2>Log in</h2> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <!-- emailの代わりにusernameを使う --> - <div class="field"> - <%= f.label :email %><br /> - <%= f.email_field :email, autofocus: true, autocomplete: "email" %> - </div> + <div class="field"> + <%= f.label :username %><br /> + <%= f.text_field :username, autofocus: true, autocomplete: "username" %> + </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "current-password" %> </div> <% if devise_mapping.rememberable? %> <div class="field"> <%= f.check_box :remember_me %> <%= f.label :remember_me %> </div> <% end %> <div class="actions"> <%= f.submit "Log in" %> </div> <% end %> <%= render "users/shared/links" %>? これで
username
でログインできるようになったよ。参考
- How To: Allow users to sign in with something other than their email address · heartcombo/devise Wiki · GitHub
- [Rails]メールアドレス以外でサインインできるようにする[devise] | もふもふ技術部
第6章 ルーティングをカスタマイズする
025 deivse_forでルーティングを定義する
? routes.rbで
devise_for
メソッドを利用すると、モジュールに対応するルーティングが自動で定義されるよ。例えばDatabase Authenticatableモジュールだけを有効にした場合、Database Authenticatableモジュールのルーティングだけがdevise_for
によって定義されるよ。app/models/user.rbclass User < ApplicationRecord devise :database_authenticatable endconfig/routes.rbRails.application.routes.draw do devise_for :users end$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy? デフォルトで有効になっている5つのモジュールを使う場合は、コントローラーが存在するDatabase Authenticatableモジュール・Recoverableモジュール・Registerableモジュールに対応するルーティングがdevise_forによって定義されるよ。
config/routes.rbRails.application.routes.draw do devise_for :users endapp/models/user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end$ rails routes Prefix Verb URI Pattern Controller#Action new_user_session GET /users/sign_in(.:format) devise/sessions#new user_session POST /users/sign_in(.:format) devise/sessions#create destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel new_user_registration GET /users/sign_up(.:format) devise/registrations#new edit_user_registration GET /users/edit(.:format) devise/registrations#edit user_registration PATCH /users(.:format) devise/registrations#update PUT /users(.:format) devise/registrations#update DELETE /users(.:format) devise/registrations#destroy POST /users(.:format) devise/registrations#create? 有効なモジュールによって自動でルーティングが変わるので注意してね。
026 devise_forをカスタマイズする
?
devise_for
にはいろんなオプションが用意されてるよ。これらのオプションを指定することでルーティングをカスタマイズできるよ。? devise_forのオプションは以下の通りだよ。
オプション 概要 利用例 controllers コントローラー変更
カスタムコントローラーを利用する際に指定する# devise/sessions -> users/sessions
devise_for :users, controllers: { sessions: "users/sessions" }path /users/sign_in
のusers
部分のpath変更# /users/sign_in -> /accounts/sing_in
devise_for :users, path: 'accounts'path_names /users/sign_in
のsign_in
部分のpath変更# /users/sign_in -> /users/login
# /users/sign_out -> /users/logout
devise_for :users, path_names: { sign_in: "login", sign_out: "logout" }sign_out_via sign_out時のHTTPメソッド # [:post, :delete]のみに変更
# デフォルト: :delete
devise_for :users, sign_out_via: [:post, :delete]only 指定コントローラーだけ有効 devise_for :users, only: :sessions skip 指定コントローラーは無効 devise_for :users, skip: :sessions class_name モデル指定 devise_for :users, class_name: 'Account' singular Userの単数形(ヘルパーに影響) # この場合、 current_admin
はcurrent_manager
になる
devise_for :admins, singular: :managerskip_helpers URLヘルパーを作らない
既存コードとのコンフリクトを避けたい場合に使う# デフォルト: false
devise_for :users, skip: [:registrations, :confirmations], skip_helpers: true
devise_for :users, skip_helpers: [:registrations, :confirmations]format (.:format)
をつける# デフォルト: true
devise_for :users, format: falsemodule コントローラーの名前空間変更 # デフォルト: "devise"
# Devise::SessionsController -> Users::SessionsController
devise_for :users, module: "users"failure_app 認証失敗時のRackアプリ(wardenのレシピ参照) constraints ルーティング成約 defaults パラメーターのデフォルト値 参考
027 名前空間を指定する
?
devise_for
メソッドはnamespace
メソッドなどのRailsの既存のメソッドと組み合わせて使えるよ。namespace
を使うと名前空間を指定できるよ。config/routes.rbnamespace :hoge do devise_for :users end$ rails routes Prefix Verb URI Pattern Controller#Action new_hoge_user_session GET /hoge/users/sign_in(.:format) hoge/sessions#new hoge_user_session POST /hoge/users/sign_in(.:format) hoge/sessions#create destroy_hoge_user_session DELETE /hoge/users/sign_out(.:format) hoge/sessions#destroy new_hoge_user_password GET /hoge/users/password/new(.:format) hoge/passwords#new edit_hoge_user_password GET /hoge/users/password/edit(.:format) hoge/passwords#edit hoge_user_password PATCH /hoge/users/password(.:format) hoge/passwords#update PUT /hoge/users/password(.:format) hoge/passwords#update POST /hoge/users/password(.:format) hoge/passwords#create cancel_hoge_user_registration GET /hoge/users/cancel(.:format) hoge/registrations#cancel new_hoge_user_registration GET /hoge/users/sign_up(.:format) hoge/registrations#new edit_hoge_user_registration GET /hoge/users/edit(.:format) hoge/registrations#edit hoge_user_registration PATCH /hoge/users(.:format) hoge/registrations#update PUT /hoge/users(.:format) hoge/registrations#update DELETE /hoge/users(.:format) hoge/registrations#destroy POST /hoge/users(.:format) hoge/registrations#create028 独自のルーティングを定義する
?? サインイン画面に
/sign_in
でアクセスしたくてこんなルーティングを定義したけどエラーになるよ。なんで?config/routes.rbget "sign_in", to: "devise/sessions#new"? Deviseのコントローラーに対して素朴にルーティングを定義するとエラーになっちゃうよ。独自のルーティングを定義するには、
devise_scope
というメソッドを使ってScopeを明示する必要があるんだ。config/routes.rb# devise_scopeを使いuser Scopeに対するルーティングであることを明示する # `users`ではなく`user`と単数形になるので注意 devise_scope :user do get "sign_in", to: "devise/sessions#new" end?
devise_for
のpath
オプションやpath_names
オプションがDeviseのルーティングを変更するのに対して、devise_scope
は新しくルーティングを追加する感じだよ。? ちなみに
devise_scope
にはas
というaliasが存在するからそっちを使ってもOKだよ。config/routes.rbas :user do get "sign_in", to: "devise/sessions#new" end参考
029 Deviseのルーティングを0から定義する
?? Deviseコントローラーに対してデフォルトのルーティングを全部なしにして、0から自分でルーティングを定義するにはどうすればいい?
?
devise_for
でデフォルトのルーティングを全部skipして、devise_scope
で好きなようにルーティングを定義していけばOKだよconfig/routes.rb# デフォルトで定義されるルーティングは全部無効にする # `:all`は全てのコントローラーを指す devise_for :users, skip: :all # あとはこの中に好きなようにルーティングを定義していけばOK devise_scope :user do get "sign_in", to: "devise/sessions#new" end? デフォルトのルーティングはなくなり、自分で定義したルーティングだけになるよ。
$ rails routes Prefix Verb URI Pattern Controller#Action sign_in GET /sign_in(.:format) devise/sessions#new? pathだけでなく
user_session_path
などのURLヘルパーも変わるので注意してね。参考
030 ログイン後とログイン前でrootのルーティングを分ける
? ログイン後とログイン前でrootのルーティングを分けたい場合は、routes.rbで
authenticated
を使えばOKだよ。config/routes.rb# ログイン後のroot。 # `authenticated`ブロック内はログイン後のユーザーに対してだけマッチする。 # どちらもURLヘルパーが`root_path`になるので、`as`オプションを使って変更してあげてね。じゃないとエラーになるよ。 # ルーティングは上から順に評価されるので、こちらを上にしてね。 authenticated do root to: 'dashboard#show', as: :authenticated_root end # ログイン前のroot。 root to: 'landing#show'? 内部的にはRailsの
constraints
を利用しているので、コントローラーで分岐させるよりスマートになるよ。? Scopeを指定することも可能だよ。
config/routes.rbauthenticated :admin do root to: 'admin/dashboard#show', as: :admin_root end root to: 'landing#show'? user.roleを指定することも可能だよ。
config/routes.rbauthenticated :user, lambda {|user| user.role == "admin"} do root to: "admin/dashboard#show", as: :user_root end root to: 'landing#show'031 ログイン後のみアクセスできるルーティングを定義する
? ログイン後のみアクセスできるルーティングを定義するには
authenticate
メソッドを使ってね。config/routes.rb# catsリソースにはログイン後でないとアクセスできない。 # ログイン前にアクセスするとroot_pathにリダイレクトされる。 authenticate do resources :cats end? コントローラーで
authenticate_user!
を使うのと同じ感じだよ。app/controllers/cats_controller.rbclass CatsController < ApplicationController before_action :authenticate_user! end?
authenticated
はログイン前だとルーティングにmatchしないのに対して、authenticate
はログイン前だとmatchした上でroot_pathにリダイレクトするよ。少し違うので注意してね。032 /users/sign_inを/users/loginに変更する
?? ログイン画面のpathを
/users/sign_in
から/users/login
に変えたいのだけど? そんな場合は
devise_for
で自動で作成されるルーティングをスキップして、代わりに自分でルーティングを定義するといいよ。config/routes.rb# devise_forで自動作成される以下の3つのルーティングをスキップ # GET /users/sign_in devise/sessions#new # POST /users/sign_in devise/sessions#create # DELETE /users/sign_out devise/sessions#destroy devise_for :users, skip: [:sessions] # 代わりに以下の3つのルーティングを自分で定義する # GET /users/login devise/sessions#new # POST /users/login devise/sessions#create # DELETE /users/logout devise/sessions#destroy devise_scope :user do get 'login' => 'devise/sessions#new', as: :new_user_session post 'login' => 'devise/sessions#create', as: :user_session get 'logout' => 'devise/sessions#destroy', as: :destroy_user_session end? 別のやり方としては、
devise_for
のpath_names
オプションを使う方法もあるよ。config/routes.rbdevise_for :users, path: '', path_names: { sign_in: 'login', sign_out: 'logout'}参考
- How To: Change the default sign_in and sign_out routes · heartcombo/devise Wiki · GitHub
- devise でログインログアウトのパスを変更したいときの注意点 - おもしろwebサービス開発日記
第7章 メーラーをカスタマイズする
033 メール内容を変更する
? メール内容をカスタマイズするにはメーラーのビューを変更すればOKだよ。 014 ビューをカスタマイズする とやり方は同じだよ。
? まずは自分のアプリにビューファイルをコピーしてね。
$ rails g devise:views invoke Devise::Generators::SharedViewsGenerator exist app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke form_for exist app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb exist app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb exist app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb exist app/views/devise/sessions create app/views/devise/sessions/new.html.erb exist app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb invoke erb exist app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb? あとは作成されたメーラーのビューファイルを自分で修正していけばOKだよ。
? メーラーのビューファイルは以下の5つだよ。
ビューファイル 概要 モジュール app/views/devise/mailer/confirmation_instructions.html.erb confirmationの指示メール Confirmable app/views/devise/mailer/email_changed.html.erb メールアドレス変更メール Database Authenticatable app/views/devise/mailer/password_change.html.erb パスワード変更メール Database Authenticatable app/views/devise/mailer/reset_password_instructions.html.erb パスワードリセットの指示メール Recoverable app/views/devise/mailer/unlock_instructions.html.erb アンロックの指示メール Lockable 034 メールのfrom欄を変更する
? Deviseで送信するメールのfrom欄を変更するには
config.mailer_sender
を設定してね。config/initializers/devise.rb- config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = 'your-address@example.com'035 メーラーをカスタマイズする
? メーラー自体をカスタマイズしたい場合は
Devise::Mailer
を継承したメーラークラスを作成するよ。app/mailers/my_mailer.rbclass MyMailer < Devise::Mailer # application_helperのヘルパーを使えるようにする helper :application # URLヘルパーを使えるようにする include Devise::Controllers::UrlHelpers # my_mailerではなくdevise/mailerのビューを使うようにする default template_path: 'devise/mailer' end? 作成したメーラークラスをDeviseのメーラーとして設定するよ。全てのモジュールでこのメーラーが使われようになるよ。
config/initializers/devise.rbconfig.mailer = "MyMailer"? あとはこんな感じでカスタマイズしたいメールをオーバーライドしてね。
app/mailers/my_mailer.rbclass MyMailer < Devise::Mailer # ...省略... # Confirmableモジュールのconfirmation指示のメール # # 引数 # record: user # token: トークン # opts: 追加オプション付きのhash def confirmation_instructions(record, token, opts={}) # ヘッダー追加 headers["Custom-header"] = "Bar" # 引数のoptsを利用するとfromなどのヘッダーをオーバーライドできる opts[:from] = 'my_custom_from@domain.com' opts[:reply_to] = 'my_custom_from@domain.com' # 元の処理をそのまま実行 super end end?
Devise::Mailer
自体は https://github.com/heartcombo/devise/blob/master/app/mailers/devise/mailer.rb に定義されてるよ。# frozen_string_literal: true if defined?(ActionMailer) class Devise::Mailer < Devise.parent_mailer.constantize include Devise::Mailers::Helpers def confirmation_instructions(record, token, opts = {}) @token = token devise_mail(record, :confirmation_instructions, opts) end def reset_password_instructions(record, token, opts = {}) @token = token devise_mail(record, :reset_password_instructions, opts) end def unlock_instructions(record, token, opts = {}) @token = token devise_mail(record, :unlock_instructions, opts) end def email_changed(record, opts = {}) devise_mail(record, :email_changed, opts) end def password_change(record, opts = {}) devise_mail(record, :password_change, opts) end end end? メーラーのメソッドは以下の通りだよ。
Database Authenticatable
メーラー#メソッド 概要 Devise::Mailer#email_changed Eメール変更完了メール。Eメール変更時に送信する。 Devise::Mailer#password_change パスワード変更完了メール。パスワード変更時に送信する。 Recoverable
メーラー#メソッド 概要 Devise::Mailer#reset_password_instructions パスワードリセットメール Confirmable
メーラー#メソッド 概要 Devise::Mailer#confirmation_instructions confirm指示メール Lockable
メーラー#メソッド 概要 Devise::Mailer#unlock_instructions アカウントアンロック指示メール 参考
036 メール送信を非同期にする
? ActiveJobを使っている場合、Deviseのメール送信もActiveJobを介して行うよ。デフォルトではDeviseのメール送信は同期送信になってるよ。
# https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise/models/authenticatable.rb#L200 def send_devise_notification(notification, *args) # mailを用意 message = devise_mailer.send(notification, self, *args) # mailを同期送信 # Remove once we move to Rails 4.2+ only. if message.respond_to?(:deliver_now) message.deliver_now else message.deliver end end? メール送信を非同期にするには、Userモデルで
send_devise_notification
をオーバーライドしてあげればOKだよ。app/models/user.rbdef send_devise_notification(notification, *args) # deliver_laterを使って非同期送信するように修正 devise_mailer.send(notification, self, *args).deliver_later end参考
- devise/authenticatable.rb at 45b831c4ea5a35914037bd27fe88b76d7b3683a4 · heartcombo/devise · GitHub
- Active Job with Rails 4 and Devise - Stack Overflow
第8章 I18nをカスタマイズする
037 メッセージを変更する
? Deviseではflashメッセージ、バリデーションエラー、メールの件名にI18nを利用しているよ。
devise.en.yml
の値を変更することで、対応するメッセージを変更できるよ。?
devise.en.yml
はこんな感じだよ。config/locales/devise.en.yml# Additional translations at https://github.com/heartcombo/devise/wiki/I18n en: devise: confirmations: confirmed: "Your email address has been successfully confirmed." send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: already_authenticated: "You are already signed in." inactive: "Your account is not activated yet." invalid: "Invalid %{authentication_keys} or password." locked: "Your account is locked." last_attempt: "You have one more attempt before your account is locked." not_found_in_database: "Invalid %{authentication_keys} or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" reset_password_instructions: subject: "Reset password instructions" unlock_instructions: subject: "Unlock instructions" email_changed: subject: "Email Changed" password_change: subject: "Password Changed" omniauth_callbacks: failure: "Could not authenticate you from %{kind} because \"%{reason}\"." success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password has been changed successfully. You are now signed in." updated_not_active: "Your password has been changed successfully." registrations: destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." signed_up: "Welcome! You have signed up successfully." signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." updated: "Your account has been updated successfully." updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again" sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." already_signed_out: "Signed out successfully." unlocks: send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: already_confirmed: "was already confirmed, please try signing in" confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" expired: "has expired, please request a new one" not_found: "not found" not_locked: "was not locked" not_saved: one: "1 error prohibited this %{resource} from being saved:" other: "%{count} errors prohibited this %{resource} from being saved:"? この値を変更することでDeviseのメッセージを変更できるよ。
038 メッセージを日本語化する
? メッセージを日本語化するには、Railsのデフォルトロケールを
:ja
に変更して、jaロケールファイルを設置すればOKだよ。? まずデフォルトロケールを日本語に設定してね。
config/application.rbconfig.i18n.default_locale = :ja? 次にjaロケールファイルを設置するよ。 https://github.com/heartcombo/devise/wiki/I18n#japanese-devisejayml に日本語化されたロケールファイルがあるので利用してね。
config/locales/devise.ja.yml# Additional translations at https://github.com/plataformatec/devise/wiki/I18n ja: devise: confirmations: confirmed: "アカウントの確認が成功しました。" send_instructions: "アカウントの確認方法をメールでご連絡します。" send_paranoid_instructions: "あなたのメールアドレスが登録済みの場合、アカウントの確認方法をメールでご連絡します。" failure: already_authenticated: "既にログイン済みです。" inactive: 'アカウントが有効になっていません。' invalid: 'メールアドレスかパスワードが違います。' locked: "アカウントがロックされています。" last_attempt: "もう一回ログインに失敗したらアカウントがロックされます。" not_found_in_database: "メールアドレスまたはパスワードが無効です。" timeout: "一定時間が経過したため、再度ログインが必要です" unauthenticated: "続けるには、ログインまたは登録(サインアップ)が必要です。" unconfirmed: "続ける前に、アカウントの確認をお願いします。" mailer: confirmation_instructions: subject: "アカウントの登録方法" reset_password_instructions: subject: "パスワードの再設定" unlock_instructions: subject: "アカウントのロック解除" omniauth_callbacks: failure: "%{kind} から承認されませんでした。理由:%{reason}" success: "%{kind} から承認されました。" passwords: no_token: "パスワードリセットのメール以外からは、このページにアクセスする事ができません。もしパスワードリセットのメールから来ている場合は、正しいURLでアクセスしていることを確認して下さい。" send_instructions: "パスワードのリセット方法をメールでご連絡します。" send_paranoid_instructions: "メールアドレスが登録済みの場合、パスワード復旧用ページヘのリンクをメールでご連絡します。" updated: "パスワードを変更しました。ログイン済みです" updated_not_active: "パスワードを変更しました。" registrations: destroyed: "アカウントを削除しました。ぜひまたのご利用をお待ちしております!" signed_up: "ようこそ!アカウント登録を受け付けました。" signed_up_but_inactive: "アカウントは登録されていますが、有効になっていないため利用できません。" signed_up_but_locked: "アカウントは登録されていますが、ロックされているため利用できません。" signed_up_but_unconfirmed: "確認メールを、登録したメールアドレス宛に送信しました。メールに記載されたリンクを開いてアカウントを有効にして下さい。" update_needs_confirmation: "アカウント情報が更新されました。新しいメールアドレスの確認が必要です。更新確認のメールを新しいメールアドレス宛に送信しましたので、メールを確認し記載されたリンクを開き、新しいメールアドレスの確認をお願いします。" updated: "アカウントが更新されました。" sessions: signed_in: "ログインしました。" signed_out: "ログアウトしました。" unlocks: send_instructions: "アカウントのロックを解除する方法をメールでご連絡します。" send_paranoid_instructions: "アカウントが存在する場合、ロックを解除する方法をメールでご連絡します。" unlocked: "アカウントのロックが解除されました。続けるにはログインして下さい。" errors: messages: already_confirmed: "は既に登録済みです。ログインしてください" confirmation_period_expired: "%{period}以内に確認する必要がありますので、新しくリクエストしてください。" expired: "有効期限切れです。新しくリクエストしてください。" not_found: "は見つかりませんでした。" not_locked: "ロックされていません。" not_saved: one: "1つのエラーにより、%{resource} を保存できませんでした:" other: "%{count} 個のエラーに せんでした:"? これでログイン時などに表示されるメッセージが日本語化されるよ。
? ビューの文言はI18nを使わずに直接英語で書かれているため、日本語化されないよ。ビューも日本語化したい場合は 039 ビューを日本語化する を参照してね。
039 ビューを日本語化する
? Deviseではflashメッセージ、バリデーションエラー、メールの件名にI18nを利用しているよ。ビューの文言はI18nを使わずに直接英語で書かれているため、 038 メッセージを日本語化する のやり方では日本語化されないんだ。
?
devise-i18n
というgemを利用すれば、I18nに対応したビューを作成できるためビューの文言も日本語化できるよ。devise-i18nではメッセージも一緒に日本語化されるため、 038 メッセージを日本語化する の手順は不要だよ。それじゃあ日本語化していくね。? まずは
devise-i18n
をインストールしてね。Gemfilegem 'devise-i18n'$ bundle install? デフォルトロケールを日本語に設定するよ。
config/application.rbconfig.i18n.default_locale = :ja? devise-i18nのジェネレーターを使って、I18n対応のビューを作成するよ。既に
$ rails g devise:views
でビューを作成している場合はコンフリクトするから事前に削除しておいてね。$ rails g devise:i18n:views invoke Devise::I18n::SharedViewsGenerator create app/views/devise/shared create app/views/devise/shared/_error_messages.html.erb create app/views/devise/shared/_links.html.erb invoke Devise::I18n::MailerViewsGenerator create app/views/devise/mailer create app/views/devise/mailer/confirmation_instructions.html.erb create app/views/devise/mailer/email_changed.html.erb create app/views/devise/mailer/password_change.html.erb create app/views/devise/mailer/reset_password_instructions.html.erb create app/views/devise/mailer/unlock_instructions.html.erb invoke i18n:form_for create app/views/devise/confirmations create app/views/devise/confirmations/new.html.erb create app/views/devise/passwords create app/views/devise/passwords/edit.html.erb create app/views/devise/passwords/new.html.erb create app/views/devise/registrations create app/views/devise/registrations/edit.html.erb create app/views/devise/registrations/new.html.erb create app/views/devise/sessions create app/views/devise/sessions/new.html.erb create app/views/devise/unlocks create app/views/devise/unlocks/new.html.erb? Deviseのジェネレーターで作成したサインイン画面と、devise-i18nのジェネレーターで作成したサインイン画面のコードを比べてみよう。
? こちらはDeviseのジェネレーターで作成したサインイン画面。
app/views/devise/sessions/new.html.erb<!-- 『Log in』などの文言が英語で直接書かれている --> <h2>Log in</h2> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "current-password" %> </div> <% if devise_mapping.rememberable? %> <div class="field"> <%= f.check_box :remember_me %> <%= f.label :remember_me %> </div> <% end %> <div class="actions"> <%= f.submit "Log in" %> </div> <% end %> <%= render "devise/shared/links" %>? こちらはdevise-i18nのジェネレーターで作成したサインイン画面。『Log in』などの文言が
t
メソッドを利用して書かれているよ。app/views/devise/sessions/new.html.erb<!-- 『Log in』などの文言が`t`メソッドを利用して書かれている --> <h2><%= t('.sign_in') %></h2> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "current-password" %> </div> <% if devise_mapping.rememberable? %> <div class="field"> <%= f.check_box :remember_me %> <%= f.label :remember_me %> </div> <% end %> <div class="actions"> <%= f.submit t('.sign_in') %> </div> <% end %> <%= render "devise/shared/links" %>? 最後にjaロケールファイルを作成するよ。
$ rails g devise:i18n:locale ja create config/locales/devise.views.ja.yml? このロケールファイルはビューのI18nだけでなくメッセージのI18nにも対応しているよ。なのでDeviseのjaロケールファイルはこれだけあればOKだよ。
config/locales/devise.views.ja.ymlja: activerecord: attributes: user: confirmation_sent_at: パスワード確認送信時刻 confirmation_token: パスワード確認用トークン confirmed_at: パスワード確認時刻 created_at: 作成日 current_password: 現在のパスワード current_sign_in_at: 現在のログイン時刻 current_sign_in_ip: 現在のログインIPアドレス email: Eメール encrypted_password: 暗号化パスワード failed_attempts: 失敗したログイン試行回数 last_sign_in_at: 最終ログイン時刻 last_sign_in_ip: 最終ログインIPアドレス locked_at: ロック時刻 password: パスワード password_confirmation: パスワード(確認用) remember_created_at: ログイン記憶時刻 remember_me: ログインを記憶する reset_password_sent_at: パスワードリセット送信時刻 reset_password_token: パスワードリセット用トークン sign_in_count: ログイン回数 unconfirmed_email: 未確認Eメール unlock_token: ロック解除用トークン updated_at: 更新日 models: user: ユーザ devise: confirmations: confirmed: メールアドレスが確認できました。 new: resend_confirmation_instructions: アカウント確認メール再送 send_instructions: アカウントの有効化について数分以内にメールでご連絡します。 send_paranoid_instructions: メールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。 failure: already_authenticated: すでにログインしています。 inactive: アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。 invalid: "%{authentication_keys}またはパスワードが違います。" last_attempt: もう一回誤るとアカウントがロックされます。 locked: アカウントは凍結されています。 not_found_in_database: "%{authentication_keys}またはパスワードが違います。" timeout: セッションがタイムアウトしました。もう一度ログインしてください。 unauthenticated: アカウント登録もしくはログインしてください。 unconfirmed: メールアドレスの本人確認が必要です。 mailer: confirmation_instructions: action: メールアドレスの確認 greeting: "%{recipient}様" instruction: 以下のリンクをクリックし、メールアドレスの確認手続を完了させてください。 subject: メールアドレス確認メール email_changed: greeting: こんにちは、%{recipient}様。 message: あなたのメール変更(%{email})のお知らせいたします。 subject: メール変更完了。 password_change: greeting: "%{recipient}様" message: パスワードが再設定されたことを通知します。 subject: パスワードの変更について reset_password_instructions: action: パスワード変更 greeting: "%{recipient}様" instruction: パスワード再設定の依頼を受けたため、メールを送信しています。下のリンクからパスワードの再設定ができます。 instruction_2: パスワード再設定の依頼をしていない場合、このメールを無視してください。 instruction_3: パスワードの再設定は、上のリンクから新しいパスワードを登録するまで完了しません。 subject: パスワードの再設定について unlock_instructions: action: アカウントのロック解除 greeting: "%{recipient}様" instruction: アカウントのロックを解除するには下のリンクをクリックしてください。 message: ログイン失敗が繰り返されたため、アカウントはロックされています。 subject: アカウントの凍結解除について omniauth_callbacks: failure: "%{kind} アカウントによる認証に失敗しました。理由:(%{reason})" success: "%{kind} アカウントによる認証に成功しました。" passwords: edit: change_my_password: パスワードを変更する change_your_password: パスワードを変更 confirm_new_password: 確認用新しいパスワード new_password: 新しいパスワード new: forgot_your_password: パスワードを忘れましたか? send_me_reset_password_instructions: パスワードの再設定方法を送信する no_token: このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。 send_instructions: パスワードの再設定について数分以内にメールでご連絡いたします。 send_paranoid_instructions: メールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。 updated: パスワードが正しく変更されました。 updated_not_active: パスワードが正しく変更されました。 registrations: destroyed: アカウントを削除しました。またのご利用をお待ちしております。 edit: are_you_sure: 本当によろしいですか? cancel_my_account: アカウント削除 currently_waiting_confirmation_for_email: "%{email} の確認待ち" leave_blank_if_you_don_t_want_to_change_it: 空欄のままなら変更しません title: "%{resource}編集" unhappy: 気に入りません update: 更新 we_need_your_current_password_to_confirm_your_changes: 変更を反映するには現在のパスワードを入力してください new: sign_up: アカウント登録 signed_up: アカウント登録が完了しました。 signed_up_but_inactive: ログインするためには、アカウントを有効化してください。 signed_up_but_locked: アカウントが凍結されているためログインできません。 signed_up_but_unconfirmed: 本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。 update_needs_confirmation: アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。 updated: アカウント情報を変更しました。 updated_but_not_signed_in: あなたのアカウントは正常に更新されましたが、パスワードが変更されたため、再度ログインしてください。 sessions: already_signed_out: 既にログアウト済みです。 new: sign_in: ログイン signed_in: ログインしました。 signed_out: ログアウトしました。 shared: links: back: 戻る didn_t_receive_confirmation_instructions: アカウント確認のメールを受け取っていませんか? didn_t_receive_unlock_instructions: アカウントの凍結解除方法のメールを受け取っていませんか? forgot_your_password: パスワードを忘れましたか? sign_in: ログイン sign_in_with_provider: "%{provider}でログイン" sign_up: アカウント登録 minimum_password_length: "(%{count}字以上)" unlocks: new: resend_unlock_instructions: アカウントの凍結解除方法を再送する send_instructions: アカウントの凍結解除方法を数分以内にメールでご連絡します。 send_paranoid_instructions: アカウントが見つかった場合、アカウントの凍結解除方法を数分以内にメールでご連絡します。 unlocked: アカウントを凍結解除しました。 errors: messages: already_confirmed: は既に登録済みです。ログインしてください。 confirmation_period_expired: の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。 expired: の有効期限が切れました。新しくリクエストしてください。 not_found: は見つかりませんでした。 not_locked: は凍結されていません。 not_saved: one: エラーが発生したため %{resource} は保存されませんでした。 other: "%{count} 件のエラーが発生したため %{resource} は保存されませんでした。"? これでビューとメッセージを日本語化できたよ。
? ちなみに複数モデルを利用していてScope対応のビュー(usersとかadminとかのやつ)が必要な場合は、Scopeを指定すればOKだよ。
$ rails g devise:i18n:views users $ rails g devise:i18n:views admins第9章 設定をカスタマイズする
040 設定を変更する
? Deviseの設定は
config/initializers/devise.rb
で変更可能だよ。? 各設定項目は日本語で説明すると以下のような感じだよ。ちなみにコメントアウトされている値が(基本的には)デフォルト値になるよ。
config/initializers/devise.rb# frozen_string_literal: true Devise.setup do |config| # Deviseが使用する秘密鍵。 # Deviseはこのキーを利用してtokenを作成する(confirmation_token、reset_password_token、unlock_token)。 # このキーを変更すると全てのtokenが無効になる。 # デフォルトではsecret_key_baseをsecret_keyとして利用する。 # config.secret_key = '48bf747d05636bd17b63751533ac6879106a058e94253754a0bfe552d60ab822ad52c25b322c93b90d7479a91fe28da84ac038f8b295d523a4c2a18c08ed9c42' # ==> Controllerの設定 # Devise::SessionsControllerなどのDeviseの各コントローラーの親クラス。 # config.parent_controller = 'DeviseController' # ==> Mailerの設定 # Mailerのfrom。 config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' # Mailerクラス # カスタムMailerを利用する場合はここを変更する。 # 詳細は『035 メーラーをカスタマイズする』を参照。 # config.mailer = 'Devise::Mailer' # Devise::Mailerの親クラス。 # config.parent_mailer = 'ActionMailer::Base' # ==> ORMの設定 # ORMをロードする。 # ActiveRcordとMongoidをサポートしている。 require 'devise/orm/active_record' # ==> 認証全般の設定 # 認証キー(ユーザーを認証する際に利用するキー)。 # email以外のキーを利用したい場合に変更する。 # 詳細は『024 emailの代わりにusernameでログインさせる』を参照。 # config.authentication_keys = [:email] # 認証に使用するリクエストオブジェクトのパラメータ。 # config.request_keys = [] # 大文字小文字を区別しない認証キー。 # Userの作成/修正/認証/検索時に大文字小文字を区別しない 。 config.case_insensitive_keys = [:email] # 空白を削除する認証キー。 # Userの作成/修正/認証/検索時に空白を削除する。 config.strip_whitespace_keys = [:email] # request.paramsによる認証を有効にする。 # `config.params_authenticatable = [:database]`とすればDB認証(メール + パスワード)認証のみを有効にする。 # config.params_authenticatable = true # HTTP Authによる認証を有効にする。 # `config.http_authenticatable = [:database]` とすればDB認証のみを有効にする。 # config.http_authenticatable = false # Ajaxリクエストに対して401を返す。 # config.http_authenticatable_on_xhr = true # Basic認証で利用されるrealm。 # config.http_authentication_realm = 'Application' # paranoidモード。 # メールアドレスが登録されているかどうかを確認するのを防ぐ。 # 詳細は https://github.com/heartcombo/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable # config.paranoid = true # userをsessionに保存する処理をスキップする箇所。 config.skip_session_storage = [:http_auth] # セキュリティーのため認証時にCSRFトークンをsessionから削除する。 # trueだとサインインやサインアップでAjaxを使用する場合、サーバーから新しいCSRFトークンを取得する必要がある。 # config.clean_up_csrf_token_on_authentication = true # eager load時にルーティングをリロードする。 # before_eager_loadフックを利用。 # falseにするとアプリ起動が高速になるが、Deviseのマッピングをロードする必要がある場合は正常に起動できない。 # config.reload_routes = true # ==> Database Authenticatableモジュールの設定 # ハッシュ化のレベル。 # ハッシュ化には結構時間がかかる。 # bcrypt(デフォルトのアルゴリズム)の場合、レベルに応じて指数関数的に遅くなり、例えばレベル20では60秒程度かかる。 # テストの時はレベル1にして速度を上げる。 # 本番ではレベル10以下は利用すべきでない。 config.stretches = Rails.env.test? ? 1 : 12 # ハッシュ化する際のpepper。 # pepperはsaltみたいなやつ。 # 詳細は https://stackoverflow.com/questions/6831796/whats-the-most-secure-possible-devise-configuration # config.pepper = '9a11b4eaf0250fec05630de0b518c3f63086fa403a8309d74408b3223d57a2312cef3ef746152f43c508da74b11cf21f982d9573ef552a186e36d83818129029' # email変更時にemail変更完了メールを送信する。 # config.send_email_changed_notification = false # password変更時にpassword変更完了メールを送信する。 # config.send_password_change_notification = false # ==> Confirmableモジュールの設定 # confirmなしでログインできる期間。 # これを設定すると一定期間はconfirm前でもログインできるようになる。 # nilに設定すると無期限にログインできるようになる。 # デフォルトは 0.days。(confirmなしにはログインできない。) # config.allow_unconfirmed_access_for = 2.days # confirmation_tokenの有効期限。 # ユーザーはこの期限内にconfirm指示メールのリンクをクリックしないといけない。 # デフォルトは nil。(制限なし。) # config.confirm_within = 3.days # サインアップ時だけでなく、email変更時にもConfirmメールを送信する。 # unconfirmed_emailカラムが必要。 config.reconfirmable = true # confirmのキー。 # config.confirmation_keys = [:email] # ==> Rememberableモジュールの設定 # Sessionが切れるまでの時間。 # デフォルトは2.weeks。 # config.remember_for = 2.weeks # ログアウト時にremember_tokenを期限切れにする。 config.expire_all_remember_me_on_sign_out = true # cookie利用時に期間を伸ばす。 # config.extend_remember_period = false # cookieにセットするオプション。 # config.rememberable_options = {} # ==> Validatableモジュールの設定 # passwordの長さ。 # Rangeで指定。この場合は6文字から128文字。 config.password_length = 6..128 # emailバリデーションで利用する正規表現 config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ # ==> Timeoutableモジュールの設定 # タイムアウト時間 # config.timeout_in = 30.minutes # ==> lockableモジュールの設定 # ロック方法 # - failed_attempts: 指定回数間違えたらロック # - none: 自動ロックはなしで、サーバ管理者が手動でロック # config.lock_strategy = :failed_attempts # アンロックのキー # config.unlock_keys = [:email] # アンロック方法 # - email: メールでアンロックのリンクを送信 # - time: 数時間後にアンロック(config.unlock_inと一緒に使う) # - both: emailとtimeの両方 # - none: 自動アンロックはなしで、サーバ管理者が手動でアンロック # config.unlock_strategy = :both # ロックまでの回数 # config.maximum_attempts = 20 # アンロックまでの時間(`config.unlock_strategy = :time`の場合) # config.unlock_in = 1.hour # ロック前に警告する # config.last_attempt_warning = true # ==> Recoverableモジュールの設定 # # パスワードリセット時にキーになるカラム。 # config.reset_password_keys = [:email] # パスワードリセットの有効期限。 config.reset_password_within = 6.hours # パスワードリセット後に自動ログイン。 # config.sign_in_after_reset_password = true # ==> devise-encryptable gemの設定 # bcrypt以外のハッシュ化アルゴリズム。 # devise-encryptable gemのインストールが必要。 # bcrypt以外のアルゴリズムは:sha1、:sha512、:clearance_sha1、:authlogic_sha512、:sha1など。 # config.encryptor = :sha512 # ==> Scopeの設定 # Scope用のビューを優先的に使うようになる。 # trueにすると`devise`名前空間のビューではなく、`users`などのScope対応のビューを利用する。 # デフォルトは高速化のため`false`に設定されている。 # 詳細は『023 複数モデルを利用する』を参照。 # config.scoped_views = false # デフォルトのScope。 # 通常であればuserになる。 # config.default_scope = :user # ログアウト時に全てのScopeでのログアウトとする。 # falseの場合は/users/sign_outでログアウトした場合、user Scopeだけログアウトになる。 # config.sign_out_all_scopes = true # ==> Navigationの設定 # ナビゲーションとして扱われるフォーマットのリスト。 # config.navigational_formats = ['*/*', :html] # ログアウト時のHTTPメソッド config.sign_out_via = :delete # ==> OmniAuthの設定 # OmniAuthの設定。 # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' # ==> Wardenの設定 # Wardenの設定。 # strategy追加したりfailure_app変更したり。 # # config.warden do |manager| # manager.intercept_401 = false # manager.default_strategies(scope: :user).unshift :some_external_strategy # end # ==> Mountable Engineの設定 # Mountable Engineで使う際のrouter名。 # config.router_name = :my_engine # # OmniAuthのpath。 # OmniAuthを利用する場合に設定する。 # config.omniauth_path_prefix = '/my_engine/users/auth' # ==> Turbolinksの設定 # Turbolinksを利用している場合、リダイレクトを正しく動作させるためにTurbolinks::Controllerをincludeする。 # # ActiveSupport.on_load(:devise_failure_app) do # include Turbolinks::Controller # end # ==> Registerableモジュールの設定 # パスワード変更後に自動的にサインインさせる。 # config.sign_in_after_change_password = true end参考
- 投稿日:2020-12-01T09:53:41+09:00
ログイン機能の実装【Rails】
はじめに
現在、転職活動用のポートフォリオ作成のため、補助金などに関する記事の閲覧・検索アプリケーションを作成しております。その際に実装したユーザー管理機能(deviseの導入)をアウトプットのため記載していきます。記事初投稿になります。
環境:Rails 6.0
1.deviseの導入
Gemfilegem 'devise'を記述後、ターミナルにて
ターミナル% bundle install % rails g devise:installのコマンドを実行し、ライブラリをインストールします。
2.モデルの作成
ターミナル% rails g devise userのコマンドにて、モデルとマイグレーションの生成やルーティングの設定を行います。
3.テーブルの作成
先ほど生成したマイグレーションファイルを使用してテーブルを作成します。
...._devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :first_name, null: false t.string :last_name, null: false t.string :first_name_kana, null: false t.string :last_name_kana, null: false t.string :email, null: false, default: "", unique: true t.string :encrypted_password, null: false, default: "" t.integer :state_id, null: false t.string :phone_number, null: false t.date :birth_day, null: false ## 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 null: false 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デフォルトでemailとpasswordのカラムは作成されているので、そのほか必要に応じて記載します(first_nameやbirth_dayなど)。
ターミナル% rails db:migrateを実行し、usersテーブルが作成できる。
4.モデルの編集
app/models/user.rbclass User < ApplicationRecord extend ActiveHash::Associations::ActiveRecordExtensions belongs_to :state has_many :sns_credentials, dependent: :destroy devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2] validates :first_name, :last_name, :first_name_kana, :last_name_kana, :state_id, :phone_number, :birth_day, presence: true PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze validates_format_of :password, with: PASSWORD_REGEX, message: 'には英字と数字の両方を含めて設定してください' validates :first_name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: 'は全角で入力してください' } validates :last_name, format: { with: /\A[ぁ-んァ-ン一-龥]/, message: 'は全角で入力してください' } validates :first_name_kana, format: { with: /\A[ァ-ヶー-]+\z/, message: 'は全角カナで入力してください' } validates :last_name_kana, presence: true, format: { with: /\A[ァ-ヶー-]+\z/, message: 'は全角カナで入力してください' } validates :phone_number, format: { with: /\A\d{11}\z/, message: 'は数字のみで入力してください' } #中略こちらにバリデーションやアソシエーションを追記していきます。
5.ビューファイルの編集
index.html.erb<header> #中略 <%# ユーザーがログインしているとき %> <% elsif user_signed_in? %> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item login-user">ようこそ、<%= current_user.last_name %>さん!</li> </ul> <ul class="navbar-nav ml-auto"> <li class="nav-item"><%= link_to '記事一覧', root_path %></li> <li class="nav-item"><%= link_to 'マイページ', "#" %></li> <li class="nav-item"><%= link_to 'ログアウト', destroy_user_session_path, method: :delete %></li> </ul> </div> <%# ログインしていないとき %> <% else %> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav ml-auto"> <li class="nav-item"><%= link_to '記事一覧', root_path %></li> <li class="nav-item"><%= link_to '新規登録', new_user_registration_path %></li> <li class="nav-item"><%= link_to 'ログイン', new_user_session_path %></li> </ul> </div> <% end %> </nav> </div> </header>今回はログインしている場合としていない場合でビューファイルを分けています。
6.ストロングパラメーターの設定
先ほどマイグレーションファイルでも記述しましたが、ユーザー情報のカラムはemailとpasswordがデフォルトで入っており、その他の任意の情報(ユーザー名など)は追記する必要があります。その際に新たな情報を渡すためにストロングパラメーターも編集しなければならないですが、deviseに関しては、処理を行うコントローラーがGemの中に記述されており、編集することができません。そのためdeviseのコントローラーにストロングパラメーターを反映する方法として、devise_parameter_sanitizerメソッドを使用しました。
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :first_name_kana, :last_name_kana, :state_id, :phone_number, :birth_day]) end end全てのコントローラーが継承しているファイルである、application_controller.rbファイルに記述しています。deviseの処理を行うコントローラーはGemの中に記述されており編集できないため、本ファイルに定義しています。
7.最後に
一通りの実装方法を記述してきましたが、不明な点や間違っている点がありましたら、ご指摘いただけると幸いです。今後はdevise実装から管理者機能の追加、SNS認証の導入も行っていますので、そのアウトプットもできたらと思っております。
- 投稿日:2020-12-01T09:36:07+09:00
devise_parameter_sanitizerメソッドについて
devise_parameter_sanitizerとは?
devise(Gem)のUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できるようになるメソッド。
また、新しく追加したカラムをdeviseに定義されているストロングパラメーターに含めたい場合は、permitメソッドを用いることで含めることができる。
さらに、deviseに関するストロングパラメーターを記述する際は、application_controller.rbに記述する。(deviseの処理を行うコントローラーはGem内に記述されており編集できないため。)使用方法
① privateにdeviseに関するストロングパラメーターの記述する。
application_controller.rbprivate def configure_permitted_parameters devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー]) end第一引数の処理名には、それぞれサインイン時やサインアップ時、アカウント情報更新時の処理を記述する。
deviseの処理名 役割 :sign_in ログインの処理を行う時 :sign_up 新規登録の処理を行う時 :account_update アカウント情報更新の処理を行う時 例)新規登録時に名前(name)を許可する記述
application_controller.rbprivate def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) end② before_actionを用いてメソッドを呼び出す。
application_controller.rbbefore_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー]) end「if:」
before_actionのオプションの一つで、値にメソッド名を指定することで、その戻り値がtrueであるときのみ処理を実行するように設定できる。
今回の場合は、「:devise_controller?」というdeviseのヘルパーメソッド名を指定して、deviseに関するコントローラーの処理であればその時だけ実行されるように設定している。
- 投稿日:2020-12-01T08:51:04+09:00
【RSpec】Ruby on Rails チュートリアル「第5章」のテストを RSpec で書いてみた
はじめに
こちらは前回の続きとなります。
【RSpec】Ruby on Rails チュートリアル「第4章」のテストを RSpec で書いてみた
https://qiita.com/t-nagaoka/items/4565a453596dfa0b0e3e対象者
- Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
- Ruby on Rails チュートリアル「第5章」のテストを Rspec で書きたいけど、書き方が分からない人
テストコード
実際にテストコードを書き換えた結果が下記になります。
Minitest
test/controllers/static_pages_controller_test.rbclass StaticPagesControllerTest < ActionDispatch::IntegrationTest test "should get home" do get root_path assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do get help_path assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do get about_path assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end test "should get contact" do get contact_path assert_response :success assert_select "title", "Contact | Ruby on Rails Tutorial Sample App" end endtest/integration/site_layout_test.rbrequire 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest test "layout links" do get root_path assert_template 'static_pages/home' assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path get contact_path assert_select "title", full_title("Contact") get signup_path assert_select "title", full_title("Sign up") end endtest/helpers/application_helper_test.rbrequire 'test_helper' class ApplicationHelperTest < ActionView::TestCase test "full title helper" do assert_equal full_title, "Ruby on Rails Tutorial Sample App" assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App" end endtest/controllers/users_controller_test.rbclass UsersControllerTest < ActionDispatch::IntegrationTest test "should get new" do get signup_path assert_response :success end endRSpec
spec/requests/static_pages_spec.rbrequire "rails_helper" RSpec.describe "StaticPages", type: :request do describe "Home page" do it "should get root" do visit root_path expect(page.status_code).to eq(200) expect(page).to have_title full_title expect(page).not_to have_title "Home |" end end describe "Help page" do it "should get help" do visit help_path expect(page.status_code).to eq(200) expect(page).to have_title full_title("Help") end end describe "About page" do it "should get about" do visit about_path expect(page.status_code).to eq(200) expect(page).to have_title full_title("About") end end describe "Contact page" do it "should get contact" do visit contact_path expect(page.status_code).to eq(200) expect(page).to have_title full_title("Contact") end end endspec/features/site_layout_spec.rbrequire 'rails_helper' RSpec.describe "SiteLayoutTest", type: :feature do before { visit root_path } subject { page } scenario "layout links" do is_expected.to have_link nil, href: root_path, count: 2 is_expected.to have_link nil, href: help_path is_expected.to have_link nil, href: about_path is_expected.to have_link nil, href: contact_path visit contact_path expect(page).to have_title full_title("Contact") visit signup_path expect(page).to have_title full_title("Sign up") end endspec/helpers/application_helper_spec.rbrequire "rails_helper" RSpec.describe ApplicationHelper, type: :helper do describe "full_title" do context 'no argument' do it 'return 「Ruby on Rails Tutorial Sample App」' do expect(full_title).to eq("Ruby on Rails Tutorial Sample App") end end context 'argument is "Help"' do it 'return 「Help | Ruby on Rails Tutorial Sample App」' do expect(full_title("Help")).to eq("Help | Ruby on Rails Tutorial Sample App") end end end endspec/requests/users_controller_spec.rbrequire 'rails_helper' RSpec.describe UsersController, type: :request do describe "GET #new" do it "returns http success" do get signup_path expect(response).to have_http_status(:success) end end endポイント
full_title ヘルパーを使用するためには、以下のように rails_helper.rb で ApplicationHelper を読み込む必要があります。
spec/rails_helper.rbRSpec.configure do |config| include ApplicationHelper次回
「第6章」のテストコードを RSpec に書き換える予定です。
- 投稿日:2020-12-01T08:05:40+09:00
ポートフォリオ(TOKYOシェア不動産)解説
アプリ概要
都内のシェアハウス物件を簡単に、シェアハウスに対するリアルな評価を参考にしながら検索できるアプリです。
- 都内にあるシェアハウスを検索できる
- 内覧に行ったことがある物件や以前住んでいた物件の口コミを点数付きで投稿する機能を実装
- 気になる物件をマイページで管理できる機能を実装
下記ページで公開中です!
https://share-house-app.herokuapp.com/このアプリを作った背景
自分が困った経験と、「こういうものがあればもっとシェアハウス入居者が増えるのでは」という思いつきから。
具体的には下記の通りです。1. シェアハウスの良さを共有したい 2. シェアハウスならではのリアルな情報を得られるサイトがない1. シェアハウスの良さを共有したい
私は22歳のときに関西から上京し、女性限定の8名住みのシェアハウスに1年半住んでいます。上京した際にシェアハウスを選んだ理由は、「初期費用も含めて安い家賃で便利な立地に住むことができるから」です。値段と立地の選択肢だけでシェアハウスを選んだのですが、体調不良や災害時にお互いに助け合うことができる、友人ができる、不在時に郵便物を受け取ってもらえる、風呂トイレなどの掃除不要など、実際に住むと思ってた以上にメリットを感じました。
一方で、シェアハウスに住んでいると話すと、驚かれることも多いこと、シェアハウス経験者が少ないことを感じました。そこから、「もっとシェアハウスの良さを知ってもらいたい、一人暮らし物件を探す時の選択肢にシェアハウスを考えてもらいたい!」
と考えるようになり、「スーモのような、シェアハウス版物件検索サイトを私が作ろう!」と考えました。2. シェアハウスならではのリアルな情報を得られるサイトがない。
私は初めてシェアハウスを探した時に、どのサイトから探したらいいのか、シェアハウスってどういう雰囲気なのかはサイトからでは全くわからず、とても苦労しました。私の友人も含めて、シェアハウスは共同生活だからこそ、人間関係、共同部分の衛生環境、騒音、匂いなど内見だけじゃわからない不安点を持って入居を始めます。
そこで、「物件ごとの入居者・内見者の口コミ情報を集約できるサイトを作れば便利なのでは?」と思い、「それを実現できるサービスを作ってみたい」と今回の作成に至りました。機能
・検索機能 ・口コミ投稿、評価機能 ・マイページ機能 ・物件管理機能 ・管理者権限検索機能
- エリア別で検索ができる
- 保存数順、家賃順、駅からの距離順で検索ができる
口コミ投稿・評価機能
- 内覧時、入居時のパターンを分けて口コミを投稿できる
マイページ機能
- 口コミ投稿した物件を一覧で閲覧できる
- 保存した物件を一覧で確認できる
物件管理機能
- 物件の所在地を地図で確認できる
管理者権限
- 物件情報の登録・編集
追加実装予定
- 検索機能の複雑化
- 部屋の家具情報ページの追加
- 物件画像を複数投稿、複数表示
- 物件投稿者へのお問い合わせ機能
- rspecを使ったテストコード
- レスポンシブ対応
- 管理者画面で口コミ集計結果を表示
- AWSにデプロイ
TrelloのURLに詳しく記載しております。https://trello.com/b/Cdp4Es2l/%E3%82%B7%E3%82%A7%E3%82%A2%E3%83%8F%E3%82%A6%E3%82%B9%E6%A4%9C%E7%B4%A2%E3%82%B5%E3%82%A4%E3%83%88
ポイント
・実際にシェアハウスに住んでいる友人とシェアハウスに興味がある友人に物件探しで重視することや心配事をヒアリングし、アプリの仕様を検討
・評価を★で表すために、jQueryのプラグイン(Raty)を使用
・Google Maps APIを使用して、店舗地図を表示
・管理者権限を持ったアカウントのみ、物件情報を扱える仕様に
・GithubでプルリクとTreroでタスク管理を丁寧に行いながら開発
・並び替え検索によって、目的に応じた検索性を向上
・物件ごとの共有設備をアイコンを使って表示
・物件ごとに部屋情報を表示使用技術等
- 言語:Ruby
- フレームワーク:Ruby on Rails
- フロント:html、Sass、JavaScript(jQuery)
- インフラ:heroku
- ソースコード管理:GitHub
- その他:Trelloでタスク管理
環境
- Ruby 2.6.6p146
- Rails 6.0.3.4
- PostgreSQL 12.3
- Heroku
使用した主なGem
- ransack :詳細な検索機能
- devise :ユーザーログイン機能
- kaminari :ページネーション機能
- geocoder :住所から経度緯度を算出
- binding.pry :デバックツール
- 投稿日:2020-12-01T05:27:02+09:00
Railsのサンプルユーザーをseeds+Fakerで用意する
Rails開発やRspecのテスト時にサンプルのユーザーを数十人用意したり、
管理者を作ったりするのはよくある事かと思います。(たぶん)しかし、名前を自分で考えたりアプリを作る度に書くのは面倒なので、自分用にseeds修正までの手順を残しておきます。
環境
MacOS Catalina
Rails 6.0.3.2
Ruby 2.6.3
mysql 8.0.22gemを導入する
Gemfileに下記を追記します。
Gemfilegroup :development, :test do gem 'faker' end忘れずに、bundle installします。
bundle install
準備は以上で完了です。
seedsファイルを修正する
db/seeds.rb# 管理者ユーザー作成 User.create!(name: "AdminUser", email: "admin@example.com", password: "12345678", password_confirmation: "12345678", admin: true) # ゲストユーザー作成 User.create!(name: "GuestUser", email: "guest@example.com", password: "12345678", password_confirmation: "12345678", guest: true) # ユーザー作成 1.upto(49) do |i| name = Faker::Name.name email = "sample-#{i}@example.com" password = 'password' User.create!(name: name, email: email, password: password, password_confirmation: password, confirmed_at: Time.zone.now, confirmation_sent_at: Time.zone.now) end上記例では、管理者ユーザー/ゲストユーザー/サンプルユーザー50人を作成しています。
サンプルユーザーの名前は、Fakerが用意しているダミーの名前になります。
他にもメールアドレスや職業なんかをFakerで作るのもアリですね。データを作る
ターミナルrails db:seedsseedsコマンドで、seeds.rbに書いたデータがDBに用意されます。
DBのデータを丸ごとseedsのデータにしたい場合は、「rails db:reset」でDBごと置き換わります。
(ただし、全てのテーブルのデータが消えるので注意)Fakerの使い方
Rubyファイル内で Faker::[ジャンル].[タイトル等] を入力すればOK。
上記seedsファイルの例にもありますが、例えばユーザーの名前を作りたい場合下記のように書けます。Faker::Name.name使いたいサンプルデータの一覧は、Faker公式のリファレンスで確認が可能です。
事前に使いたいデータがどんなデータが知りたい場合、Railsコンソールで確認しておくのがラクです。rails c Loading development environment (Rails 6.0.3.2) [1]pry(main)> Faker::Name.name => "佐野 陽斗"Fakerデータの日本語化について
Fakerは、デフォルトでは英語のデータしか利用できません。
日本語を利用する場合は、i18nを利用し日本語化すればOKです。日本語化の手順は、下記の記事が参考になりました!
[初学者]Railsのi18nによる日本語化対応終わりに
ラクにサンプルデータが用意できて、Fakerマジで最高である。
- 投稿日:2020-12-01T04:07:56+09:00
RailsでDBパスワード等を環境変数(.env)で管理する
DBのパスワードやAPIのアクセスキー等の重要な情報は、
Gitにあげたくなかったり開発環境と本番環境で異なっていたりします。
そういった情報の管理によく利用されるのが環境変数というものらしい。Railsで環境変数を利用する場合、大まかに下記2パターンある様子。
①.bash_profileに記述する
②dotenv-railsというgemを利用し.envに記述する今回は、②のenvファイルを利用する方法を自分用に残しておきます。
間違い等あれば、ご指摘いただければ幸いです。環境
MacOS Catalina
Rails 6.0.3.2
Ruby 2.6.3
mysql 8.0.22gemを導入する
Gemfileに下記を追記します。
Gemfilegem 'dotenv-rails'
忘れずに、bundle installします。
bundle install
.envファイルを作成する
Railsアプリのルートディレクトリに移動し、下記を実行。
touch .env
.envファイルを修正し、管理したいキーを追加します。
.envDB_NAME=RailsApp_production DB_USERNAME=root DB_PASSWORD=12345678 DB_HOSTNAME=hogehoge○○=hogehoge の○○の部分がキーになるので、使用する際に分かりやすいキー名に設定します。
設定した環境変数を使用する
ENV['キー名']で呼び出すことができます。
ENV['DB_NAME'] ENV['DB_USERNAME']環境変数をgitにあげないようにする
せっかく環境変数を利用しているのに、gitにあげちゃったら元も子もありません。
.gitignoreに下記の行を追記し、.envがgitにコミットされないようにします。
(既に設定していれば修正不要).gitignore.env終わり
思った以上に簡単に利用することができますね。
環境変数は多くのRailsアプリで利用することになると思うので、できればこの記事を見なくても使えるようにしたい所。
.bash_profileを利用する場合はgemを導入する必要がないのでラクそうだけど、利用手順はちょっと忘れそうな感じがします…。
- 投稿日:2020-12-01T03:54:01+09:00
未経験からのエンジニア転職を目指す人の学習記録②
こんにちは。
前回からの進捗状況を報告致します。
同様に現職の仕事もしながらプログラミング学習をしている方やこれから勉強を始める方への参考になれば幸いです。①M1 MacBookAirを購入
表題の通り、M1 MacBookAirを購入しました。
GPU7コアでメモリだけ16GBにカスタマイズしたものです。ただやはり言われているようにDockerやVirtualBoxなどの仮想環境ツールは動作せず。
届いたのが22日の日曜日でしたがこの日と次の月曜日はほぼ開発環境を整えるのに時間を費やしました(笑)
色々試行錯誤して、HomeBrewなどダウンロードできたもののRails -s でローカルサーバーが立たず、、、徹夜でやってもうまくいかず一旦環境作りは諦めましたね、、、
②Progate JavaScriptまで一周終了、ただ、、、
気を取り直してProgateを再開しましたが、RailsコースをVあたりでわけわからなくなったので中断してしまいました、、、
アプリ版でちょこちょこやってたJavaScriptコースを終わらせる方にシフトし、ちょうど終了した形になります。
これから
フロントエンド系(HTML&CSS、JacaScript)
バックエンド系(Ruby,SQL)
その他必須なもの(Git、GitHub)をざっとですが、学習したので
アウトプットを行っていこうと思います。まずは作成物を紹介するためのポートフォリオサイトを作っていこうかと思います。
まとめ
Progateがひと段落着いた感じがするので(頭に入っているかは微妙)
本格的に自分で何かを「作る」という作業に時間を割いていければと思っております。
- 投稿日:2020-12-01T00:56:25+09:00
Railsエンジニアとして入社前に知りたかったコマンドたち(シチュエーション付き)
この記事は DeNA 21 新卒 Advent Calendar 2020 の3日目の記事です。
はじめに
この記事はふと、コマンドだけのまとめではなくもっと具体的なシチュエーションが想像できる記事を書いてみたら面白いんじゃないかと思い書くことにしたものです
また自分がRails開発を経験した中で役に立ったと感じた知識にも触れ、メモ欄にはその解説を加えてみました。
ザックリ評価基準( 個人的 )
- 超重要☆☆☆☆☆
- 重要 ☆☆☆☆
- 普通 ☆☆☆
目次
index タイトル( ※リンクになってます ) 1 $ tail -F devlopment.logs
2 $ git checkout -b <ブランチ名>
3 $ git push --set-upstream origin <ブランチ名>
4 $ rails db:migrate
5 $ bundle install
1.
$ tail -F devlopment.logs
☆☆☆☆☆わい「すいません、、ここで何で落ちてしまっているのか分からず..orz」
上司「ログの方は確認しましたか?」
わい「(...ん?ログってどうやって見るんだ?)」使い方
tail
コマンドを実行したことでログが監視される様子↓メモ・注意点
development.log
の役割
Ruby on Railsでは開発環境で動作させている場合ログがlog
配下のdevelopment.log
に溜まっていきます(最初これ知った時驚いた)
また詳しいロガーに仕様や設定に関してはRailsガイドを参考にするともっと詳しく知れたりします。
参考: Rails アプリケーションのデバッグ 2.1 ロガーについて
tail
コマンド
tail
コマンドはLinuxコマンドのうちの1つでオプションに-F
もしくは-f
を付けることで 指定ファイルの追記を監視する ことができます。
オプションのf
の大文字小文字の違いに関してだと以下の記事だとこちらがおすすめだったりします
参考: tailコマンドのオプション「f」と「F」
また注意点としてtail
コマンドを用いる際にちゃんとディレクトリがlog
配下に位置していることを忘れずに!
ルートディレクトリから誤ってコマンドを実行すると存在しないと怒られてしまいますゆえ
アプリのルートディレクトリから叩くことにしてtail -f log/devlopment.logs
としても良いかも知れません
2.
$ git checkout -b <ブランチ名>
☆☆☆上司「現在の状態を知りたいのでWIPでいいので適当なタイミングでPR作ってもらえますか?」
わい「はい!(あれPRってどうやって作るんだっけ?)」使い方
まずPRを作る前に新しくブランチを作り、切り替えた様子
メモ・注意点
WIP とか PR とかの用語
ここで上司役が言っていた WIP とは Work In Progress の略称で 作業途中である状態 を指す用語として良く聞く単語だったりします。
またGithub の Pull Request をPR(ぴーあーる)と呼ばれたりするのも複数社で観測したのでシチュエーションに載せてみました。
またそのまま 「プルリクエストを作ってください」 とも良く聞きます。
時代はもう
git switch -c <ブランチ名>
?
実はGitのブランチの切り替え方法に関しては Git 2.23 から実験的にgit checkout -b <ブランチ名>
からgit switch -c <ブランチ名>
にしたらどうか言う提案がされました。
参考: 【Git】あなたが知らない新コマンドswitch/restoreの世界にご招待
確かにブランチを切り替える際にswitch
するのとcheckout
するのとではswitch
の方が直感的で分かりやすいですよね。( 後そもそもcheckout
役割多すぎ問題..
またおまけでswitch
ver. も載せておきます
3.
$ git push --set-upstream origin <ブランチ名>
☆☆☆わい「(おし、とりあえずブランチは切れたっと。あれ、でも
add
してcommit
したのになんでpushできないんだ)」
幻聴「エラーログをちゃんと読みなさい」
わい「( ゚Д゚)ハッ!?(今、何か聞こえた!?)」使い方
メモ・注意点
ブランチを切り替えた後の最初のPush
個人開発ではもしかしたら全て1つのブランチ( masterブランチとか )で作業していることがありますよね?( 特にGit初めたてとかしてました← )
その場合はとりあえず
1.git add <file_name>
2.git commit -m <commit_name>
3.git push
をすることでGitを使いこなしているつもりなっていたりしました。
しかし、ブランチを切り替えた際の挙動はちょっと違ったりします。
実際には$ git push --set-upstream origin <ブランチ名>
のコマンドでさっきまではmasterに向けて届けてたかも知れないけど、今度は切り替えた先のブランチに届けますよっと宣言しています。
また、おまけでpush
が成功した後ブラウザ上でPRを作成する様子を載せておこうと思います(ほんとに初めここ分からなかったなぁ..)
4.
$ rails db:migrate
☆☆☆☆☆わい「すいません、追加された差分を
git pull
して更新したあとエラーが..」
上司「もしかしてmigrate
してないんじゃないんですか?」
わい「あ。。(それだ!)」使い方
メモ・注意点
差分を追加した際にマイグレーションファイルが追加されているとき
差分を拾ってきてそれでマイグレーションのエラーを引き起こすことは複数人で開発をしていると良く起きたりします。
初め自分はgit pull
してきて平気なときもあれば、なぜかこうやってActiveRecord::PendingMigrationError
になってしまうときがあるなと、同じことをしているのになぜこう言ったことが起こるのか分かっていませんでした。
しかしよく観察してみるとマイグレーションを要求されることはそう言った変更を行ったときのみ要求されるものだと気付き、それから更新した際にマイグレーションファイルが追加や削除、変更されている場合には自分の開発環境も揃えてあげる必要があるんだと知っていきました。5.
$ bundle install
☆☆☆☆☆わい「すいません、たしかにマイグレーションエラーは消えたのですが、、」
上司「もしかして次はbundile install
してないとか言わないですよね?」
わい「( ゚Д゚)ハッ!?。。(やばいそれだ!!)」使い方
メモ・注意点
ブランチ切り替えにおけるgemのアップデートが必要な場合
複数人でのチーム開発を行っている際には他のエンジニアの方が新しい gem を導入するケースは当然あります。
その場合、手元の自分の環境でも同じ最新状態に揃えるためにGemfileならびにGemfile.lockを更新する必要があります。
おわりに
Railsでの開発と言うのは一言で言うのはあまりにも広く、開発組織によって様々な使われ方がされると思います。モノリシックに作って一旦VueやReactは入れずに作るんだ!ってところもあれば、RailsにはAPIサーバーとしての役割を持ってもらって、フロントはフロントで頑張りますってところも当然あるかと。
また、Webサービスを作る手段として良く知られている気がしますがRailsをゲーム開発に用いることもあります。
そして最近はDockerの利用も主流になってきて、初学者からするとdockerのコマンドとRailsのコマンドを合わせて利用する場面もありすぐに理解が追いつかず詰まりどころが多くなった印象もありました。今回はそう言ったところで何かRails開発に慣れていく手助けができればと思い書いてみました
宣伝
この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!
また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい!
Follow @DeNAxTech
- 投稿日:2020-12-01T00:18:15+09:00
Railsでテーブルの値を、別テーブルの項目で並び替える方法
こんばんは
アロハな男、やすのりです!今日は、Railsでモデルやテーブルを扱っていく上で1度は必ず通るであろう
sort_byメソッド
について書いていこうと思います。
ただ普通に配列を並び替えわけではなく、今回はアソシエーションを組んでいる別テーブルの値
で並び替えをする方法になります!!結論
並び替える方法が2種類あります。
全く該当しない物は扱わない
モデル名.joins(:並び替えたい項目があるテーブル名).group(:まとめたい項目).order('count(並び替えたい項目)')
該当しないものも扱いたい
テーブル情報を入れた変数.sort_by { |x| x.並び替えたい項目があるテーブル名 }となります。
テーブル構造
例えば以下の様なテーブルがあった場合で考えます。
- experiencesテーブル(色んなお店やレジャー施設情報保存用)
- usersテーブル(登録していただいているユーザー様の情報保存用)
- favoritesテーブル(ユーザー様がお店等をお気に入りしたことを保存)
favoritesテーブルでは、他2つのidを外部キーとして持っています。
ユーザー様がお店やレジャー施設をお気に入り登録すると、
id user_id experience_id 1 1 1 2 1 3 3 2 1 4 3 2 5 2 2 こんな具合にテーブルに値が保存されていきます。
なぜ2種類?
それでは、なぜ方法が2種類あるのかを見ていきたいと思います。
まず
全く該当しない物は扱わない
場合ですが、
こちらの重要部分はjoinsメソッド
です。詳しいことは↓こちらを見ていただく方がよりわかりやすいかと思います。
【Rails】joinsメソッドのテーブル結合からネストまでの解説書簡単に説明すると、例えば
Experience.joins(:favorites)とすると、
experiencesテーブルとfavoritesテーブルの両方共に存在しているレコード情報
しか検索できなくなってしまいます。つまり、お気に入り登録をまだされていないお店・レジャー施設は検索対象から外す。ということになります。
検索対象から外さない場合の物は後述します。続いて、
Experience.joins(:favorites).group(:experience_id).order('count(user_id) DESC')と、記述することによりお気に入りされている数が多い順でお店等を取得することができます。
groupメソッド
でexperience_idを指定することにより、複数お気に入り登録されているお店等を1つのレコードにまとめることができます。そして
orderメソッド
で並び替えをするんですが、メソッド内でcount(user_id)
とすることによってお店等がお気に入り登録されている数で並び替えることができます。ただこの方法ですと、お気に入り登録されている場所だけをピックアップして、そこから人気店を探す!!とかなら使用できるメソッドですが、お店等を全て一覧で表示したいから除外されてしまうのは...という場合もあると思います。
『それなら
joinsメソッド
を抜いてコードを書けばいいじゃん!!』となりそうなんですが、その後の
groupメソッド
ではexperience_idを、countメソッド
ではuser_idを使用しています。
この2つのidはfavoritesテーブル
が持っているテーブルですので、Experienceモデルだけでは使用できません...その場合には、もう1種類の方法です。
全て取得する方法
該当しないものも扱いたい
場合ですが、こちらは先ほどまで説明していた方法とは少し違います。
具体的にはモデルではなくテーブルに対して並び替えをする
ということです。1度変数等にレコード情報を格納してしまいます(今回は
@experiences
とします。)そして変数に対して
sort_byメソッド
を使用します。@experiences.sort_by! { |exp| exp.favorites.length }.reverse!と記述することで、全件のお店等を対象にお気に入り数が多い順で並び替えることができます。
sort_byメソッド
を使用することで変数(配列)の並び替えをでき、{}
内でその値を1つずつ取り出して評価していきます。
experiencesテーブル
とfavoritesテーブル
はアソシエーションを組んでますので、exp.favorites.lengthと記述することで、1つずつ取り出してきたお店のお気に入り数を評価して並び替えをしてくれます。
ただこのままだと昇順での並び替えになってしまいますので、最後にreverseメソッド
で結果を反転させています。最後に
最初は全部モデルに対してだけで並び替えができると思い込んでいたので、すごく時間がかかってしまいました...
いや、もしかしたらこれ以外にもっといい方法があるかも...?
ご指摘・アドバイスはどんどんいただきたいと思っていますので、コメントお待ちしています!!
- 投稿日:2020-12-01T00:01:26+09:00
【Rails】ポートフォリオに動画投稿機能を実装してみた【AWS S3 + EC2 + CarrierWave + Fog】
はじめに
本記事は 駆け出しエンジニアの第一歩!AdventCalendar2020 1日目の記事です。
転職活動に向けて個人アプリを開発しました。
マジシャン向けの動画投稿SNSサイトです。今回は動画投稿機能について自分の理解を深めるためにもまとめてみようと思います!
Railsを学習し始めて3ヶ月ほどの若輩者ゆえ、必死に色々調べながらなんとか実装できたレベルなので、もし間違っているところや、こうした方が良いというところがあればご意見いただけますと幸いです…!
動画投稿機能概要
アップロード画面
アップロード画面の[ファイルを選択]をクリックし、動画をアップロードします。
タイトル、説明、タグを入力して、[アップロード]をクリックすると投稿されます。
投稿詳細・再生画面
投稿の詳細画面のサムネイルをクリックすると動画が再生されます。
どうやって作ったか
開発環境
IDE:Cloud9
Ruby:2.6.3
Rails:5.2.4
ストレージ:Amazon S3Gemfile
Gemfilegem 'carrierwave' gem 'fog-aws' gem 'dotenv-rails'carrierwaveは動画のアップローダを実装するときに必要なgemです。
fog-awsは外部のストレージ(Amazon S3)へのアップロードを可能にするgemです。
dotenv-railsは環境変数を管理する事が出来るgemです。後ほど登場するキーなどのデリケートな情報を公に漏洩させないようにするために使います。STEP1 投稿フォームを作る
まずは形から作りましょうということで動画のアップロード部分のビューのコードです。
(タイトル、説明、タグのフォームは本記事の主旨とは若干異なるので省略しています。)
下記例の場合、Magicモデルの中にvideoカラムを作成し、そこに動画をアップロードしています。form.html.erb<%= form_with model: @magic, url: magics_path, local: true do |f| %> <%= f.label "動画のアップロード" %> <%= f.file_field :video, :accept => 'video/*' %> <%= f.submit "アップロード" %> <% end %>次に動画のアップローダを作成します。
下記コマンドを実行すると、app/uploaders/video_uploader.rbが作成されます。
こちらはcarrierwaveのコマンドであり、ここで作成したアップローダを後ほどAmazon S3にアクセスできるようカスタマイズする流れとなります。ターミナルrails g uploader video
作成したアップローダをMagicモデルのvideoカラムに紐付けます。
magic.rb# モデルとアップローダの紐付け mount_uploader :video, VideoUploaderSTEP2 Amazon S3を作成する
Amazon S3は動画を保管する倉庫の役割を担います。
AWSコンソールにログインし、サービスより[S3]をクリックします。
Amazon S3のバケット画面が表示されたら、[バケットを作成]をクリックします。バケット名に任意の名前を入力し、リージョンは最寄りのリージョンを選択します。
その他の設定についてはデフォルトの状態のまま、画面下部のバケットの作成をクリックします。
STEP3 IAMユーザーを作成する
AWS IAMユーザーはS3バケットにアクセスするためのアカウントだと思っていただければと思います。
ここで作ったユーザーを介してRailsアプリがS3にアクセスするイメージです。AWSコンソールにログインし、サービスよりIAMをクリックします。
IAMの画面が表示されたら左メニューより[ユーザー]をクリックし、[ユーザーを追加]をクリックします。任意のユーザー名を入力し、プログラムによるアクセスにチェックを入れて、[次のステップ: アクセス権限]をクリックします。
次にこのユーザーのアクセス許可の設定をします。
今回はS3へのアクセスを許可する設定を行うので、[既存のポリシーを直接アタッチ] をクリックします。
その後、S3へのアクセスポリシーを探すために、ポリシーのフィルタの右の検索バーに[S3]と入力します。
そうすると、[AmazonS3FullAccess] というポリシーが見つかりますので、こちらにチェックを入れて、[次のステップ: タグ]をクリックします。AmazonS3FullAccessは文字通りAmazon S3への全てアクセスを許可してしまいますので、必要に応じてもう少し厳しめのポリシーを設定する方が良いかもしれません。。。
これでAmazon S3へ接続するためのユーザーは作成完了です!
作成完了画面が表示されたら、[アクセスキー] と[シークレットキー]をメモしておきましょう!
念のためCSVもダウンロードしておきましょう!
これらのキーはfogがAWSに接続するために必要になってきます。STEP4 fogを使ってAmazon S3へアクセスする
まずはSTEP1で作成したアップローダの設定を行います。
stoeage :fogとすることで動画の保存先をfogの接続先にすることができます。
store_dirにはS3バケットのどのディレクトリに保存するかを記述します。
下記のように書くと、保存先はuploads/モデル名/投稿IDディレクトリに保存されます。app/uploaders/video_uploader.rbclass VideoUploader < CarrierWave::Uploader::Base (省略) # Choose what kind of storage to use for this uploader: # storage :file storage :fog (省略) # 動画ごとに保存するディレクトリを変える def store_dir "uploads/#{model.class.to_s.underscore}/#{model.id}/#{mounted_as}" end endちなみにstoeage :fileとすると、 public配下に動画が保存されます。
そのため、開発環境やテスト環境はpublicに、本番環境のみS3に保存したい場合は下記のように書きましょう。app/uploaders/video_uploader.rbclass VideoUploader < CarrierWave::Uploader::Base (省略) # developmentとtest以外はS3を使用 if Rails.env.development? || Rails.env.test? storage :file else storage :fog end (省略) # 動画ごとに保存するディレクトリを変える def store_dir "uploads/#{model.class.to_s.underscore}/#{model.id}/#{mounted_as}" end end次にfogの設定を行っていきます。
fogの設定は/config/initializers/carrierwave.rbで行うのですが、
もしファイルが存在しなければ、新規に作成しましょう。こちらのファイルの内容をざっくりと説明すると、fogの接続先(Amazon S3)を指定しています。
/config/initializers/carrierwave.rb# CarrierWaveの設定呼び出し require 'carrierwave/storage/fog' CarrierWave.configure do |config| if Rails.env.production? # 本番環境の場合 config.fog_provider = 'fog/aws' config.fog_directory = 'STEP2で作成したバケット名' config.fog_credentials = { provider: 'AWS', aws_access_key_id: ENV['AWS_ACCESS_KEY'], aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], region: 'STEP2で作成したバケットのリージョン', path_style: true } end #日本語ファイル名の設定 CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ config.cache_dir = "#{Rails.root}/tmp/uploads" endさてコードの↓部分ですが、STEP3で作成したユーザーのアクセスキーとシークレットキーを格納することになります。
ただ、これらの情報はGithubなどにアップロードすべきではない情報であるため、環境変数に格納します。
今回はdotenv-railsというgemを使って、.envファイルに環境変数を書いていきます。/config/initializers/carrierwave.rbaws_access_key_id: ENV['AWS_ACCESS_KEY'], aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],.envAWS_ACCESS_KEY="STEP3で作成したユーザーのアクセスキー" AWS_SECRET_ACCESS_KEY="STEP3で作成したユーザーのシークレットキー"STEP5 コントローラを設定する
ここまで来ればほぼ完成なのですが、最後にアプリの投稿・表示部分のコントローラーのコードも一応載せておきます。
ご自身の投稿用のコントローラに合わせて実装していただけたらと思います。app/controllers/magics_controller.rbclass MagicsController < ApplicationController (省略) def show # IDに基づく投稿を取得 @magic = Magic.find(params[:id]) end def create # 投稿内容を取得 @magic = Magic.new(magic_params) if @magic.save flash[:notice] = "動画を投稿しました。" redirect_to magic_path(@magic) else # エラーが発生した場合 render :new end end (省略) end動画を表示する部分のビューはこちらになります。
※本アプリ内ではFFmpegを用いて自動でサムネイルを作成しているのでimage_tagを使って表示していますが、この部分はご自身のアプリに合わせて変えていただければと思います。show.html.erb<%= link_to @magic.video_url.to_s do %> <%= image_tag(@magic.video_url(:screenshot).to_s ,:size => '256x144', id: "video", class: "magic-image", :alt => "screenshot") %> <% end %>まとめ
RailsアプリからAmazon S3に動画を保存する際の全体の動きをまとめるとこのようになります。
この記事を書く1週間前、泣きながら実装していたのが早くも懐かしく感じます。
これから動画投稿アプリを作ってみようという方の参考になれば嬉しいです!参考サイト
【Rails】CarrierWaveによる画像投稿 【AWS EC2】 - Qiita
【Rails5】Carrierwave + Fog で AWS S3 に画像をアップロードする | RemoNote
Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する - Qiita
【Rails】Carrierwaveとfog-awsを用いて、画像をAWS S3へアップロードする方法 - Qiita
Rails gem CarrierWave + fog を利用して AWS S3 へ画像をアップロードする - Qiita