20210411のRailsに関する記事は23件です。

【Rails】非同期通信でチャット/DM機能を実装する

ユーザー同士1対1のチャット機能を非同期通信(Ajax)で実装する方法をまとめています! お気づきの点などあれば、コメント頂戴できれば幸いです。 前提 deviseでUserモデルを作成している 各ユーザーの詳細画面(show)が作成されている(ユーザーの詳細画面にチャットへのリンクを作ります)  ?以上の前提で進めていきます 使用するモデル User = ユーザーの定義 Chat = チャットの定義 Room = チャットルームの定義 UserRoom = ユーザーとチャットルームの関連付けの定義(中間テーブル) 大まかな流れ モデル生成とアソシエーション コントローラー作成 & ルーティング設定 chats_controllerに記述 チャットのビューを記述 create.js.erbファイルを作成&記述 STEP1: モデル生成とアソシエーション まずUserモデル以外に必要なモデルを作成していきましょう! terminal $ rails g model Room $ rails g model Chat user_id:integer room_id:integer message:text $ rails g model UserRoom user_id:integer room_id:integer $ rails db:migrate モデルが生成できたら、アソシエーションを設定します。 (それぞれ前後の記述は省略しています) app/models/user.rb has_many :user_rooms, dependent: :destroy has_many :chats, dependent: :destroy app/models/user_room.rb belongs_to :user belongs_to :room app/models/room.rb has_many :chats has_many :user_rooms #1つのルームにいるユーザ数は2人のためhas_manyになる app/models/chat.rb belongs_to :user belongs_to :room ?user_roomsテーブルはusersテーブルとroomsテーブルの中間テーブルです。 外部キーとしてuser_idとroom_idを持っています。  user_roomsテーブルは、ユーザーとルームの紐づけを行います:  - ユーザーにどのroom_idが紐づいているか  - ルームにどのuser_idが紐づいているか 例えば、AさんとBさんのチャットの場合、 - Aさん(user_id=3)がroom_id=1 - Bさん(user_id=4)がroom_id=1 であれば、AさんとBさんは共通のroom_idを持ちます。 STEP2: コントローラー作成 & ルーティング設定 では、chats_controllerとshowアクションを一緒に作成します? terminal $ rails g controller chats show 今回は、chats_controllerのshowアクションでチャットを行うので、以下のようにルーティングを設定しておきましょう。 ちなみにcreateアクションは後でコントローラに定義するように、コメントの投稿に必要になってきます。 config/routes.rb get 'chat/:id', to: 'chats#show', as: 'chat' resources :chats, only: [:create] STEP3: chats_controllerに記述 まずはコメントなしの記述内容がこちら? app/controllers/chats_controller.rb class ChatsController < ApplicationController def show @user = User.find(params[:id]) rooms = current_user.user_rooms.pluck(:room_id) user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms) if user_rooms.nil? @room = Room.new @room.save UserRoom.create(user_id: @user.id, room_id: @room.id) UserRoom.create(user_id: current_user.id, room_id: @room.id) else @room = user_rooms.room end @chats = @room.chats @chat = Chat.new(room_id: @room.id) end def create @chat = current_user.chats.new(chat_params) @chat.save end private def chat_params params.require(:chat).permit(:message, :room_id) end end それぞれ何をしているかコメントを入れたものがこちらです? ※ちなみに、どちらのユーザーに関する記述なのかがわかるように「Aさん」「Bさん」がコメントに出てきますが、「AさんがBさんに対してチャットする」想定です。つまり、Aさんがcurrent_userになります。 app/controllers/chats_controller.rb class ChatsController < ApplicationController def show #BさんのUser情報を取得 @user = User.find(params[:id]) #user_roomsテーブルのuser_idがAさんのレコードのroom_idを配列で取得 rooms = current_user.user_rooms.pluck(:room_id) #user_idがBさん(@user)で、room_idがAさんの属するroom_id(配列)となるuser_roomsテーブルのレコードを取得して、user_room変数に格納 #これによって、AさんとBさんに共通のroom_idが存在していれば、その共通のroom_idとBさんのuser_idがuser_room変数に格納される(1レコード)。存在しなければ、nilになる。 user_room = UserRoom.find_by(user_id: @user.id, room_id: rooms) #user_roomでルームを取得できなかった(AさんとBさんのチャットがまだ存在しない)場合の処理 if user_room.nil? #roomのidを採番 @room = Room.new @room.save #採番したroomのidを使って、user_roomのレコードを2人分(Aさん用、Bさん用)作る(=AさんとBさんに共通のroom_idを作る) #Bさんの@user.idをuser_idとして、@room.idをroom_idとして、UserRoomモデルのがラムに保存(1レコード) UserRoom.create(user_id: @user.id, room_id: @room.id) #Aさんのcurrent_user.idをuser_idとして、@room.idをroom_idとして、UserRoomモデルのがラムに保存(1レコード) UserRoom.create(user_id: current_user.id, room_id: @room.id) else #user_roomに紐づくroomsテーブルのレコードを@roomに格納 @room = user_room.room end #@roomに紐づくchatsテーブルのレコードを@chatsに格納 @chats = @room.chats #form_withでチャットを送信する際に必要な空のインスタンス #ここで@room.idを@chatに代入しておかないと、form_withで記述するroom_idに値が渡らない @chat = Chat.new(room_id: @room.id) end def create @chat = current_user.chats.new(chat_params) @chat.save end private def chat_params params.require(:chat).permit(:message, :room_id) end end STEP4: チャットのビューを記述 チャット画面を記述していきましょう? なおform_withは非同期通信処理のためremote: trueにしています。 以下の記述でチャット画面はこんな感じになります ※ビューの記述は練習用ということで最低限にしてあります。  スタイリッシュな感じにぜひ仕上げてください。 app/app/views/chats/show.html.erb <div class="container"> <div class="row"> <div class="col-xs-6"> <h2>CHAT WITH <%= @user.name %></h2> <table class="message table"> <thead> <tr> <th style="text-align: left; font-size: 20px;"><%= current_user.name %></th> <th style="text-align: right; font-size: 20px;"><%= @user.name %></th> </tr> </thead> <% @chats.each do |chat| %> <% if chat.user_id == current_user.id %> <tbody> <tr> <th> <p style="text-align: left;"><%= chat.message %></p> </th> <th></th> </tr> <% else %> <tr> <th></th> <th> <p style="text-align: right;"><%= chat.message %></p> </th> </tr> </tbody> <% end %> <% end %> </table> <%= form_with model: @chat, remote: true do |f| %> <%= f.text_field :message %> <%= f.hidden_field :room_id %> <%= f.submit "SEND", class:"btn btn-sm btn-success chat-btn" %> <% end %> </div> </div> </div> さて、ユーザーの詳細画面(show)から、そのユーザーとチャットができるように、ビューに記述を加えましょう? app/app/views/chats/show.html.erb <!-- current_userと@userが一致していない場合に`Begin Chat`を表示 --> <% if current_user != @user %> <%= link_to 'Begin Chat', chat_path(@user.id) %> <% end %> もしusers_controllerのshowアクションに、@userが定義されていない場合は、忘れず記述してください? app/controllers/users_controller.rb def show @user = User.find(params[:id]) end STEP5. create.js.erbファイルを作成&記述 最後に、非同期通信の設定をしていきましょう。 といってもすでにform_withにremote: trueを記述したので、あとはcreate.js.erbファイルを作成&記述するだけです。 手動で構わないので、views/chatsフォルダ直下にcreate.js.erbファイルを作成して、以下を記述しましょう。 app/views/chats/create.js.erb /*.messageを部分更新。appendメソッドで要素を追加 */ $('.message').append("<p style='text-align: left;'><%= @chat.message %></p>"); /*チャット投稿後、フォームの値に前回の投稿内容が残らないようにする */ $('input[type=text]').val("") これで完成です!おつかれさまでした。 参考資料 【Rails】DM(チャット)機能 + Ajax 実装! 【Ruby on Rails】DM、チャット機能
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails RSpec コントローラーテストでログイン状態にする

rails初学者です。 RSpecでコントローラーのテストを実装するにあたり、新規投稿画面にアクセスした場合(newアクション)のテストをログイン/非ログイン状態で分岐させたく、方法を調べたので備忘録としてまとめました。 初学者向けの内容になっています。 環境 Ruby 2.6.5 Rails 6.0.3 以下インストール済み Devise 4.7.3 RSpec 4.0.0 FactoryBot 前提条件 deviseのauthenticate_userを使って、postコントローラーのnewアクションに非ログイン状態でアクセスすると、ログイン画面にリダレクトされるようにしています。 posts_controller.rb class PostsController < ApplicationController before_action :authenticate_user!, only: [:new] ~中略~ def new @post = Post.new end ~中略~ end こちらの機能をテストしていきます。 Postsコントローラーテスト RSpecでDeviseを使えるようにする。 spec/rails_helper.rb RSpec.configure do |config| ~中略~ #下のコードを追記 config.include Devise::Test::IntegrationHelpers, type: :request end こちらのコードをrails_helper.rbに記述することで、requestテスト時にdeviseのヘルパーを呼び出すことができます。 テスト用ユーザーデータを用意 FactoryBotを使ってユーザーのテスト用データを用意します。 factories/users.rb FactoryBot.define do factory :user do email { "test@example.com" } password { "123456" } password_confirmation { password } end end ユーザーモデルにnameカラムなどを追加している場合は追加したカラムのデータも記述します。 Fakerを使ってダミーデータを生成しても良いです。 posts_spec.rbを作成 spec/requests/posts_spec.rb require 'rails_helper' RSpec.describe "Posts", type: :request do before do @user = FactoryBot.create(:user) #FactoryBotを利用してuserデータを作成 end describe 'GET #new' do context "ログインしている場合" do    #サインインする before do sign_in @user end it '正常にレスポンスが返ってくる' do get new_post_path expect(response.status).to eq 200 #正常にレスポンスが返されHTTPステータス200が発生 end end context "ログインしていない場合" do it 'ログインページにリダイレクトされる' do get new_post_path expect(response.status).to eq 302 ##リダイレクトされHTTPステータス302が発生 end end end 先ほどrails_helperにDeviseをincludeしたのでsign_inというヘルパーが利用でき、ログイン状態を作ることができます。ログインしている場合とログインしていない場合とで、sign_inを使い分けます。 before do sign_in @user end あとはeqマッチャを使って、response.statusを指定します。 #正常にレスポンスが返ってきた場合 expect(response.status).to eq 200 #リダイレクトされた場合 expect(response.status).to eq 302 (レスポンスステータスは https://developer.mozilla.org/ja/docs/Web/HTTP/Status 参照) これでテストを実行すると、テストが成功するはずです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Let's Encryptの更新(手動)

Let's Encryptの手動更新の方法(備忘録) ホントは自動更新したいけどcronがうまく機能しないので仕方なく手動でやる(気が向いたら自動更新も調べる) rootユーザーで入る [ec2-user@ip-172-31-36-245 git_toreka]$ sudo -i 下記コマンドを打つ [root@ip-172-31-36-245 ~]# /usr/local/bin/certbot-auto renew --post-hook "sudo service httpd restart" こんな感じで出たらOK Upgrading certbot-auto 1.11.0 to 1.14.0... Replacing certbot-auto... Your system is not supported by certbot-auto anymore. certbot-auto and its Certbot installation will no longer receive updates. You will not receive any bug fixes including those fixing server compatibility or security problems. Please visit https://certbot.eff.org/ to check for other alternatives. Saving debug log to /var/log/letsencrypt/letsencrypt.log - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Processing /etc/letsencrypt/renewal/torekabodymake.com.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Cert not yet due for renewal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The following certificates are not due for renewal yet: /etc/letsencrypt/live/torekabodymake.com/fullchain.pem expires on 2021-07-10 (skipped) No renewals were attempted. No hooks were run. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2021-07-10と3ヶ月後の日付が書いてあるので成功してる(はず) 自動更新ができるようになるまではとりあえずコレで行きます。 少し前にやったことがどんどん忘れていってしまうので、なにかやったらこうやって簡単にでも記録するようにします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6とdocker-composeを利用した環境にjQueryを導入する方法

docker-compose上に構築したRails6の環境に、jQueryを導入しようと色々調べたのでメモを残します。 Rails5以前のやり方を真似してGemを導入しても動かず、しばらくハマりました...。 Rails6ではwebpackerを使って以下の手順で簡単に導入できるみたいです。 まずはjQueryをインストール webpackerで管理する場合はyarnコマンドを使ってインストールします。 docker-compose上でインストールするためdocker-compose exec webコマンドを使用します。 ターミナル % docker-compose exec web yarn add jquery 無事にインストールできたらdocer-composeを立ち上げましょう。 ターミナル % docker-compose up 次にwebpackの設定を追加 jQueryをWebpackの管理下にするために設定ファイルを編集します。 config/webpack/environment.js const { environment } = require('@rails/webpacker') // ここから const webpack = require('webpack') environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery' }) ) // ここまで追記 module.exports = environment application.jsに設定を追加 application.jsのファイルを編集してjQueryを呼び出せるように設定します。 javascript/packs/application.js //中略 require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") // 追記 require('jquery') //中略 これでjQueryの導入は完了です。 jQueryの動作確認 jQueryが正常に動作するか確認します。 何らかのviewにテスト用のpタグを記入しましょう。 お好きなviewファイル.html.erb <p>テスト</p> 当然ですがブラウザには以下のように表示されます。 次に、別ファイルにjQueryの動作を記入します。 Rails6ではapp/javascript以下にjsファイルを置くようなので、ここでは直下に仮でtest.jsファイルを作成します。 app/javascript/test.js $(document).ready(function() { $("p").text("jQuery稼働テスト成功!"); }); 作成したtest.jsをviewに読み込むにはapplication.jsにjsファイル読み込みのためのコードを追記する必要があります。 javascript/packs/application.js //中略 require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") require('jquery') // 追記 require("test.js") //中略 jQueryの読み込みが成功すると以下のようにpタグの文字がjQueryによって置き換えられます。 このように表示されたらjQueryの動作確認は成功です。 これで無事にdocker-composeとRails6の環境にjQueryを導入することができました。 おわりに RailsへのjQuery導入の記事は多くありますが、Rails5以前のやり方を参照していたことでうまく行かずにしばらく悩みました。 初心者あるあるかもしれませんが、Railsのバージョンが違うと導入方法が全く違うということを痛感しつつ、なんとかjQueryを導入することができました。 同じく初心者で悩んでいる人の参考になれば幸いです。 参考にさせていただいた記事はこちら https://qiita.com/tatsuhiko-nakayama/items/b2f0c77e794ca8c9bd74 https://qiita.com/iiyama_makoto/items/940b130fd20540fd70f1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Formオブジェクトパターンについて

はじめに Formオブジェクトパターンについて、簡潔にまとめてみました。 細かい部分の説明は省略していますが、大まかな概要についてはこの記事で理解ができるかと思います Formオブジェクトパターンとは Formオブジェクトパターンとは、Railsの開発における実装パターンのことで、有名なものとして、1つのフォームから複数のモデルを操作する際に使います。 もう少しイメージしやすくすると、例えば個人情報に関するモデルと、商品情報に関するモデルの合体版モデルを擬似的に作成するといった感じです。 メリットはなんなのか? 例えば、1つのフォームから個人情報と商品情報を入力させるとします。このとき、送られた情報は個人情報モデルのバリデーションと商品情報のバリデーション、それぞれで処理を行います。この時点で、すでに非効率ですが、例えば個人情報モデルでも商品情報モデルでもバリデーションの通過ができずエラーが発生したときに、 それぞれのエラーメッセージを表示させないとダメです。 これはめんどくさい… そんな時に、合体版のようなモデル、Formオブジェクト使えば、バリデーションなどの処理は一括だが保存の際はそれぞれのDBに保存してくれるわけです。 実際に作成してみよう 以下に2つのモデルがあるとします user.rb class User < ApplicationRecord has_many :items validates :name,presence:true # 数字3桁、ハイフン、数字4桁の並びのみ許可する validates :postal_code, presence: true, format: {with: /\A[0-9]{3}-[0-9]{4}\z/, message: "is invalid. Include hyphen(-)"} item.rb class User < ApplicationRecord belongs_to :user validates :item_name,presence:true これら2つのモデルのまえに、合体版のようなモデル user_item.rbを作成します。 user_item.rb class UserItem include ActiveModel::Model attr_accessor :name,:postal_code,:item_user def save User.create(user: user,:postal_code: postal_code) Item.create(item_name:item_name) end end クラスにActiveModel::Modelをincludeすると、そのクラスのインスタンスはActiveRecordを継承したクラスのインスタンスと同様に form_with や render などのヘルパーメソッドの引数として扱え、バリデーションの機能を使用できるようになります。 なお、元々のモデルに記載したvalidatesは必要ないので削除しましょう。 このように、Formオブジェクトを使用することで、一括でバリデーションをかけることができます。 おわりに 用事帰りに車の中で執筆しました。 とても酔いましたが、自分でも新しい発見があったのでアウトプット大事ですね。 誰かのためになれば嬉しいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Railsサーバーを別のポート番号で起動しようとしたら、Could not find server "-p 8081". Run `rails server --help` for more options.と言われた時の対処法

症状 ReactAPIモード+Reactのアプリで同時にサーバーを動かすとき同一のポート番号が使用できないため、Rails側のポート側を変更しようとコマンドを打ったところ下記のエラーが表示されてしまいました。 ターミナル rails s -p 8081 DEPRECATION WARNING: Passing the Rack server name as a regular argument is deprecated and will be removed in the next Rails version. Please, use the -u option instead. (called from <top (required)> at /home/ubuntu/environment/プロジェクト名/bin/rails:9) Could not find server "-p 8081". Run `rails server --help` for more options. 翻訳すると、 「非推奨の警告:Rackサーバー名を通常の引数として渡すことは非推奨であり、次のRailsバージョンで削除される予定です。代わりに-uオプションを使用してください。 (/ home / ubuntu / environment / uber-eats-like / bin / rails:9のから呼び出されます)」 「サーバー「-p8081」が見つかりませんでした。その他のオプションについては、 rails server--helpを実行してください。」 コマンドが正しく認識されていないような感じでエラーが出ているようでした。 解決策 コマンドの空白が全角になっていたため、エラーが表示されていました。 具体的には「-p 8081」の間に空白が全角になっていました。 コマンドの空白を半角にすると、8081ポートを使って起動できました。 ターミナル rails s -p 8081 => Booting Puma => Rails 6.0.3.6 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.6.3-p62), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:8081 * Listening on tcp://[::1]:8081 Use Ctrl-C to stop 参考 RailsでポートとIPアドレスを指定する方法 https://qiita.com/Tocyuki/items/de66987ead2183e4fcae
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE Messaging APIとLIFFとRailsの組み合わせ方がわからなすぎる

LIFFで立ち上げてもらったLIFFブラウザから、LINEのユーザー情報を受け取りユーザー認証を完了させ、Rails側にデータを格納し、LINE Messaging APIでユーザーAが指定したLINE友達にその公式LINE botからPushメッセージを送る事ができる、という公式LINEアプリ?を開発しようとしています。 のですが、正直RailsでAPI連携をするのも、LINE API自体も初めてすぎてかなり手間取ってます。 まずは公式が発表している資料やSDK ・Ruby用のSDK(ソフトウェア開発キット) https://github.com/line/line-bot-sdk-ruby ↓な感じでgemとして追加もできるらしい。(参照) gem 'line-bot-api', github: 'stivan622/line-bot-sdk-ruby' 戸惑いポイント①LINEボットとチャネル(LINEログイン・LINE Messaging API)の関係と使い分け LINE Messaging APIと検索すると、LINEボットに関する情報ばかり出てきます。 ですが、私の実現させたいアプリはボットではありません。 LINE_BOT_Development_Guidelines 7p/60pより転載 LINEログインを選択=>LIFF作成、LINE Messaging API=公式LINEアカウント作成 LINEチャネルを新規作成する際、LINEログインとLINE Messaging APIというのが選べます。 LINE Messaging APIはそのままなのでわかると思いますが、LIFFを使う際は、LINEログインを選択します。 さらに、公式LINEを使ってLIFFを立ち上げたい、という場合は、必ず同じプロバイダー下にLINEログインチャネルとLINE Messaging APIチャネルをどちらも作成しなければなりません。 発行されたLIFF URLをリッチメニューのリンク先に設定したり、LIFF IDをnode.jsやGASなどに利用してMessaging APIを連携と取ったりします。(引用元: 【覚書】LIFF IDの取得方法) LINE Messaging APIを選択すると、公式LINEのQRコードが発行できたり、APIチャネルからLINE Account Managerというまたちょっと別のサイトに飛んでリッチメニューを設定できたりします。 Webhookって何? Webhookとは、相手のサービス(この記事ではLINE)でイベントが発生したら、事前に設定しておいたURLにイベントの内容をPOSTしてもらう仕組みのことです。 例えば、自分の作ったサービスの/callbackというURLをLINEのチャネルに設定しておけば、チャネルにメッセージが来た時に/callbackにメッセージの内容をPOSTしてもらう事が出来ます。 ただし、LINEのサーバーからアクセス出来るURLである必要があるので、サービスを実際に公開していないときちんと動作しません。例えば開発中のlocalhost:4567は自分のPCからしかアクセス出来ないURLなので、localhost:4567/callbackを設定した状態でメッセージが来てもlocalhost:4567/callbackは呼び出されません。 この仕様があるため、LINE Botを開発する時は基本的に毎回デプロイする必要があります。 ポート開放をすればいちいちデプロイする手間は省けますが、セキュリティ上のリスクもあるのでここでは紹介しません。 LINE push通知の実装ってどうやるん まだ実装してないけどここら辺が参考になりそう ネットで見つけたハンズオン達 環境変数とは何か、Botの登録の仕方から始まり、オウム返しをするbotを作ります。 ・【第1回】LINE×Ruby on Railsで作ろう!シゴトに生かすLINE Bot! 第二回では、フレックスメッセージとリッチメニューの設定を行えます。 ・【第2回】LINE×Ruby on Railsで作ろう!シゴトに生かすLINE Bot! 第三回では、楽天レシピAPIと連携させメッセージに料理名が送られてきたときにその料理のレシピをフレックスメッセージで送る、という実装をしています。 【第3回】LINE×Ruby on Railsで作ろう!シゴトに生かすLINE Bot! Ruby + SinatraでLINE Botを作ろう – Part 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ransackで#Array:〜〜のエラー

問題 配列処理したいが、以下のエラーが発生する。 #[Array:0x00007f55f06941f8](array:0x00007f55f06941f8) 原因 Arrayクラスになっているのが原因 ransackではActiveRecord_Relationクラスにする必要がある。 解決策 where、mapをつかい、Array→ActiveRecord_Relationクラスに変換する .where(id: items.map{ |item| [item.id](http://item.id/) }) 参考サイト 配列をActiveRecord::Relationで再取得するメソッドを作ってみる - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの環境構築の備忘録(アプリケーション導入編)

VSCode(Visual Studio Code)のインストール VSCodeダウンロードページからエディタをダウンロード ↓ エディタをアプリケーションフォルダに移動 ↓ Dockに配置 ↓ エディタを起動 ↓ [システム環境設定]を開き、[セキュリティとプライバシー]をクリック ↓ [一般]タブを開き、画面下部の「ダウンロードしたアプリケーションの実行許可」にある[このまま開く]をクリック テキストエディタの拡張機能導入 日本語設定 起動したウィンドウ画面左側の、アイコンメニュー内にある四角のアイコンをクリック ↓ 左上の「Search Extensions in Marketplace」に「Japanese Language Pack for Visual Studio Code」と入力 Japanese Language Pack for Visual Studio Codeを選択し、install(またはインストール)をクリックする 右下のRestart Nowをクリックして再起動すると、日本語設定になっている HTMLとCSSの自動補完機能の追加 起動したウィンドウ画面左側の、アイコンメニュー内にある四角のアイコンをクリック ↓ 左上の「Marketplaceで拡張機能を検索する」に「HTML Snippets」と入力 ↓ HTML Snippetsを選択し、install(またはインストール)をクリックする ↓ インストールが完了して、アンインストールの表示がされれば完了 Rubyの構文チェック機能を追加 起動したウィンドウ画面左側の、アイコンメニュー内にある四角のアイコンをクリック ↓ 左上の検索欄に「Ruby」と入力 ↓ Rubyを選択し、install(またはインストール)をクリックする ↓ インストールが完了して、アンインストールの表示がされれば完了 全角スペースのチェック機能の追加 起動したウィンドウ画面左側の、アイコンメニュー内にある四角のアイコンをクリック ↓ 左上の検索欄に「zenkaku」と入力 ↓ zenkakuを選択し、install(またはインストール)をクリックする ↓ インストールが完了して、アンインストールの表示がされればインストール完了 ↓ command + shift + pの3つのキーを同時入力し、コマンドパレットという設定ファイルの検索画面を開く 「> Enable zenkaku」と入力して選択する ↓ command + Qの2つのキーを同時入力し、VSCodeを終了する。 zenkakuの機能を常時ONにするための設定 finderでホームディレクトリを開く ↓ command + shift + .で隠しディレクトリを表示する ↓ .vscode > extensions > mosapride.zenkaku-0.0.3 > と移動する ↓ extension.jsを2本指タップしてメニューを表示する ↓ 「このアプリケーションを開く」から「テキストエディット」を選択する ↓ 5行目の「var enabled = false;」を「var enabled = true;」に変更してcommand + Sで保存する コードのスペルチェック機能の追加 起動したウィンドウ画面左側の、アイコンメニュー内にある四角のアイコンをクリック ↓ 左上の「Marketplaceで拡張機能を検索する」に「Code Spell Checker」と入力 ↓ Code Spell Checkerを選択し、install(またはインストール)をクリックする ↓ インストールが完了して、アンインストールの表示がされれば完了 tabキーで入力される半角スペースの数を設定 VSCodeのサイドバーより、「管理(下部の歯車マーク)」→ 「設定」の順に選択 Editor: Tab Sizeを2に設定 Editor: Render Whitespaceで「all」を選択 オートセーブ設定 VSCodeのサイドバーより、「管理(下部の歯車マーク)」→ 「設定」の順に選択 オートセーブの設定で「onFocusChange」を選択 これで半分終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

full_title(page_title = ‘’)を考える。yieldとprovideも。

Railsチュートリアル 第4章で登場する full_title(page_title = ‘’) に対して、色々?となったので調べてみました。 full_titleとは? ヘルパーメソッドです。確認してみます。 app/helpers/application_helper.rb module ApplicationHelper # ページごとの完全なタイトルを返します。 def full_title(page_title = '') base_title = "Ruby on Rails Tutorial Sample App" if page_title.empty? base_title else page_title + " | " + base_title end end end これをレイアウトで使います。 app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></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 %> </body> </html> 改めて full_title(page_title = ‘’) 引数の中で変数を定義している・・・?なにこれ? そんなことできるんだという感じだが、チュートリアルではたいした説明がされていません。 調べてみると、どうやらデフォルト引数というものらしい。 参考になりました。 https://www.javadrive.jp/ruby/method/index4.html どうやら、文字通り引数にデフォルトで引数に値が与えられていて、そのままデフォルトのままでもいいし、新しく引数を渡してもいいというもののようです。 今回はデフォルトではnillが渡されていますね。 では、次はレイアウトで呼び出している方での full_title(yield(:title)) を見ていきます。 yeildとは? yeildとは?
 =ブロック処理を呼び出す。 ではブロックとは?(each文とかで見たことあるな〜) =メソッドの引数として渡すことができる処理の集まり。 らしい…。 では、yield使い方を見てみます。 分かりやすいhelpのviewと、先ほどのlayoutを簡略化して見てみます。 app/views/static_pages/help.html.erb <% provide(:title, "Help") %> <h1>Help</h1> <p> Get help on the Ruby on Rails Tutorial at the <a href="https://railstutorial.jp/help">Rails Tutorial help section</a>. To get help on this sample app, see the <a href="https://railstutorial.jp/#ebook"><em>Ruby on Rails Tutorial</em> book</a>. </p> app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> . . . </head> <body> <%= yield %> </body> </html> 先ほどの復習で、yieldは引数として与えられたブロック(処理の集まり)を呼びだしています。 viewでは、ブロックはviewファイルになるらしく、今回は、help.html.erbファイルです。 yield(:titile)は、:titileとラベル付けしたブロックを呼び出すらしいです。 provideメソッド ではどうやってラベル付けするのかということでprovideメソッドを使います。 <% provide(:title, "Help") %> これは、”Help”という文字列に、:titleというラベルをつけています。 これでlayoutに、yield、yield(:titile)で何が呼び出されているか分かりました。(たぶん) P.S. 初投稿です。ご指摘ありましたらお願いします m(_ _)m Provideメソッドはググってもあんまり出てこないけど、実務ではあんまり使わないのかな? もっとしっかりRubyを理解しないとな〜と思い、空き時間にプロを目指す人のためのRuby入門読んでます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

after_initialzeを使用時には、実行条件をつけてあげよう

はじめに 現在Railsを使って開発中のアプリケーションで、after_initializeを使用する機会があった。 正直コールバックは苦手意識があって、避けてきたのもあるのとafter_initializeに関しては使い所がイマイチわからなかったため、無知の状態であった。 いざ使ってみると便利だが、少し気をつけたいポイントもあるので、本記事に記述する after_initializeとは その名のと通り、設定したクラスのオブジェクトがインスタンス化された後に実行される処理のことである。 以下にかんたんな使用例を示す。 Userクラスのオブジェクトがインスタンス化された後に、name属性に予め値をセットしている。 class User < ApplicationRecord after_initialize do self.name = "default name" end end Railsガイドには以下のように説明がされている。 after_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出されます。インスタンス化は、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われます。これを利用すれば、Active Recordのinitializeメソッドを直接オーバーライドせずに済みます。 ここで重要なのが、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われます。という部分になる。 これは本記事のタイトルの部分にもつながる重要な部分である。 なぜ実行条件をつけるのか? 今記事の目的となる部分です。Railsガイドの説明を読み解くと、User.newだけではなくUser.allやUser.where等でもafter_initializeは実行されることになります。 実はnewだけでなく、allや whereでもインスタンスは生成されている。 つまりafter_initializeを設定していると取得件数分だけ処理が実行されることになる。 DBに10000件ユーザーのデータ入っていたとして、User.allを実行すると、after_initialzeが同じ件数だけ実行される。 これは処理速やパフォーマンスに影響が出かねない。 また既存データ取得時にself.name = "default name"のようにデータに触る処理を書いてしまうと、既存データが予期しない値が入ってしまうことも考えられえるため、非常に危ない。 Railsには,FW側で便利なメソッドが揃っているため、その力をお借りして実行条件を付与しよう 私の場合は、新規データ作成時にのみ実行をしたかったためnew_record?という、インスタンス化したオブジェクトが新規か既存データかを判定してくれるメソッドを使うことで、実行条件を付与した class User < ApplicationRecord after_initialize :set_default_name, if: :new_record? def set_default_name self.name = "default name" end end 本当にインスタンスを生成しているのか? 余談だが、allやwhereの戻り値が本当にインスタンスが生成されているのかを確認したい場合の方へ。 Rubyにはinstance_of?という対象簿のオブジェクトが引数に指定したクラスのインスタンスを生成しているのか、判定するメソッドがある。 pry(main)> users = User.all pry(main)> users[0].instance_of? User => true 普段使わない機能を使う事で新たな気付きがあるので、今後も積極的に使っていきたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6】WebpackerによるBootstrap導入

 はじめに この記事ではWebpackerによるBootstrapの導入方法を説明します。 bootstrap, jQuery, popper.jsのインストール  ターミナル上で以下のコマンドを実行する。 yarn add jquery bootstrap popper.js    これによって、 package.jsonファイルに次の3行追加される。 package.json "bootstrap": "^4.6.0", "jquery": "^3.6.0", "popper.js": "^1.16.1", Bootstrapを利用できるようにする app/javascript/packs/application.jsに、 以下の2行を追加する。 app/javascript/packs/application.js import 'bootstrap' import '../stylesheets/application'    新しいディレクトリ、ファイルを追加する。 まずは以下のコマンドでapp/javascriptにstylesheetsディレクトリの作成。 mkdir app/javascript/stylesheets    app/javascript/stylesheetsにapplication.scssファイルを作成。 touch app/javascript/stylesheets/application.scss    次はapp/javascript/stylesheets/application.scssに 以下のコードを追加する。 app/javascript/stylesheets/application.scss @import '~bootstrap/scss/bootstrap'; jQueryとPopper.jsを利用できるようにする 次にapp/config/webpack/environment.jsを修正。 app/config/webpack/environment.js const { environment } = require('@rails/webpacker') //ここから const webpack = require('webpack') environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ $: 'jquery/src/jquery', jQuery: 'jquery/src/jquery', Popper: ['popper.js', 'default'] }) ) //ここまでを追加。 module.exports = environment    レイアウトを修正する app/views/layouts/application.erb.htmlの 「stylesheet_link_tag」を「stylesheet_pack_tag」に変更する。 app/views/layouts/application.html.erb <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>    これでbootstrapが利用可能になります。    注意点 bootstrapのドキュメントを参考にする場合はバージョンにあったものを使用すること。でないとjQueryが動かない場合がある。 参考にした記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】コールスタックを調査したい時(デバック)

Rubyのコードを読んでいて、「これはどれから呼ばれているんだろう?」 と思ったときに良い方法があったので、忘備録として残しておく。 組み込み関数callerを使う callerを使う事で呼び出し元の情報を文字列の配列として取得できる。 サンプルプログラム(callstack_test.rb) class TestClass def foo bar end def bar baz end def baz puts caller(0) end end TestClass.new.foo rubyを実行 $ ruby callstack_test.rb test.ruby:11:in `baz' test.ruby:7:in `bar' test.ruby:3:in `foo' test.ruby:15:in `<main>' callerの引数の指定によって、呼び出し元を変更できます。 (呼び出し元を含める場合は0を指定します。) 引数によって呼び出し元を変えることもできるが0と1以外はそんなに使わないと思う。 caller(0)で実行(呼び出し元を含める) test.ruby:11:in `baz' test.ruby:7:in `bar' test.ruby:3:in `foo' test.ruby:15:in `<main>' caller(1)で実行(呼び出し元を含めない) test.ruby:7:in `bar' test.ruby:3:in `foo' test.ruby:15:in `<main>' caller(2)で実行(引数によって呼び出し元を指定できる) test.ruby:3:in `foo' test.ruby:15:in `<main>' 参考文献 Ruby 2.5.0 リファレンスマニュアル https://rurema.clear-code.com/2.5.0/method/Kernel/m/caller.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6/Docker/MySQL + Elasticsearch 環境構築

はじめに Elasticsearchを触りたいと思ったのだが、Dockerでの環境構築で詰まったので今後のためにメモ。 環境 Rails6・Docker・MySQLによる環境構築 自分の備忘録用に書いたものだが、ここに+elasticsearchを加えていく。 以下のファイル内のmyappは自分が作成したディレクトリ名に置き換える(今回はelasticsearch-railsと命名) Dockerfile FROM ruby:2.7.1 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list # myappを自分のアプリ名に変更 RUN apt-get update -qq && apt-get install -y nodejs yarn RUN mkdir /elasticsearch-rails WORKDIR /elasticsearch-rails COPY Gemfile /elasticsearch-rails/Gemfile COPY Gemfile.lock /elasticsearch-rails/Gemfile.lock RUN bundle install COPY . /elasticsearch-rails # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] Gemfile source 'https://rubygems.org' gem 'rails', '6.0.3' Gemfile.lock # 空のままで entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" docker-compose.yml version: "3" services: # Elasticsearch用のコンテナ elasticsearch: # 下の1行でも環境構築は出来るが、日本語を扱うときに必要なプラグイン(kuromoji)を入れるために、 # elasticsearch用のDockerfileを作成 # image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1 build: context: . dockerfile: Dockerfile-elasticsearch environment: - discovery.type=single-node - cluster.name=docker-cluster - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 ports: - 9200:9200 volumes: - esdata:/usr/share/elasticsearch/data # Kibana用のコンテナ kibana: # elasticsearchとkibanaのimageのバージョン番号を一致 image: docker.elastic.co/kibana/kibana:7.10.1 ports: - 5601:5601 depends_on: - elasticsearch db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password ports: - '3306:3306' command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: # myappを自分のアプリ名に変更 - .:/elasticsearch-rails ports: - "3000:3000" depends_on: - db # elasticsearchのコンテナと関連づける - elasticsearch stdin_open: true tty: true volumes: esdata: mysql-data: elasticsearchとKibanaを追加したので所々変更。 rials用のコンテナ(web)のdepends_onに、elasticsearchのコンテナ名を追加するのを忘れないように Dockerfild-elasticsearch FROM docker.elastic.co/elasticsearch/elasticsearch:7.10.1 # 日本語をあつかうときに使うプラグイン RUN bin/elasticsearch-plugin install analysis-kuromoji # 国際的に規約されてる文字の解析器 RUN bin/elasticsearch-plugin install analysis-icu アプリ作成 --database=mysqlを忘れると、 config/database.ymlのadapterがmysql2ではなくsqliteになってしまったので注意 --force:「強制的に」と言う意味 --no-deps:リンクされたコンテナを起動しない --skip-test:minitestの作成を防ぐ。 --api:APIモードでRailsプロジェクトを作成し、APIには不要なView・UI関連のライブラリがインストールされない。 --webpacker:webpacker をインストール docker-compose run web rails new . --force --no-deps --database=mysql --webpacker 私の場合、ここで下のエラーが発生 response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"rails\": executable file not found in $PATH": unknown ここでbuildしたら治るとのことだったので実行。 docker-compose build 詳しいことは分かっていないが解決したので、仕切り直して先に進める docker-compose run web rails new . --force --no-deps --database=mysql --webpacker docker-compose build config/databas.yml変更 config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV.fetch("MYSQL_USERNAME", "root") %> password: <%= ENV.fetch("MYSQL_PASSWORD", "password") %> host: <%= ENV.fetch("MYSQL_HOST", "db") %> development: <<: *default database: elasticsearch_rails_development test: <<: *default database: elasticsearch_rails_test production: <<: *default database: elasticsearch_rails_production username: elasticsearch_rails password: <%= ENV['ELASTICSEARCH_RAILS_DATABASE_PASSWORD'] %> DBを作成する docker-compose run --rm web bundle exec rails db:create 起動し、動作確認する docker-compose build docker-compose up Rails確認 http://localhost:3000 にアクセスして、お馴染みの画面が出ればOK Kibana確認 http://localhost:5601 にアクセスし、ページが表示されればOK Elasticsearch確認 Curlコマンドでリクエストを投げ、下記のようなクラスタやバージョン情報が含まれるレスポンス返ってくればOK $ curl -XGET http://localhost:9200/ { "name" : "7d712c1f3298", "cluster_name" : "docker-cluster", "cluster_uuid" : "fA4Th3p8RNqMzyJxUlwrYw", "version" : { "number" : "7.10.1", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "1c34507e66d7db1211f66f3513706fdf548736aa", "build_date" : "2020-12-05T01:00:33.671820Z", "build_snapshot" : false, "lucene_version" : "8.7.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } 参考記事 DockerでRails,Postgres,ElasticSearchの開発環境を構築する方法 docker環境でのRailsアプリの立ち上げ RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成 おわりに 毎度毎度環境構築で詰まりますね(ーー;)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのコントローラーでインスタンス変数を使う時

初歩的なことですので こちらに書いてありましたので抜粋します メソッド間でのデータの受け渡し(典型的には、before_actionでデータをロードしておくとか) ビューへのデータの受け渡し newアクションとcreateアクションでインスタンス変数(@item)を定義 def new @item = Item.new end def create @item = Item.new(item_params) if @item.save redirect_to root_path else render :new end end これをform_withの引数へ受け渡す <h2 class="items-sell-title">商品の情報を入力</h2> <%= form_with model: @item, url: items_path, local: true do |f| %> フォームで入力されたデータをcreateアクションで生成、保存する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] eachとmapの違い

eachとmapの違いについてまとめました. each 繰り返し処理を行う際に使用する.ただ,戻り値は繰り返し処理の前の値となる. 使用例 array = [1, 5] array.each do |item| item * 2 end 2 10 => [1, 5] map 繰り返し処理の結果を配列として保持したい場合に使用する. 使用例 array = [1, 5] result = array.map do |item| item * 2 end =>[2, 10] mapをeachに書き換える方法 array = [1, 5] result = [] array.each do |item| result << item * 2 end =>[2, 10] 上記のように書き換えるとeachでmapと同じ処理を実行することができるようになる. しかし,mapに比べると記述量が多くなってしまうので,繰り返し処理の結果を使用する際はmapで記述するようにする. 参考リンク 【Ruby】eachとmapの違い
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者向け rails db:migrateのエラー

開発環境 ・Mac OS Big Sur 11.2 ・Ruby 2.6.5 ・Rails 6.0.0 エラーの状況 アイディアを管理するためのAPIを、railsを用いて作成。 ideasテーブル、categoriesテーブルを作成し、rails db:migrateコマンドを実行した結果、以下のようなエラーが出ました。 == 20210411003522 CreateIdeas: migrating ====================================== -- create_table(:ideas) rails aborted! StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Table 'category_app_development.categories' doesn't exist エラー内容を見ると、"Mysql2::Error: Table 'category_app_development.categories' doesn't exist" とあり、categoriesテーブルが存在しない、と指摘されています。 ideasテーブル、categoriesテーブルをそれぞれ見てみます。 20210411003529_create_categories.rb class CreateCategories < ActiveRecord::Migration[6.0] def change create_table :categories do |t| t.string :name, null: false t.timestamps end end end 20210411003522_create_ideas.rb class CreateIdeas < ActiveRecord::Migration[6.0] def change create_table :ideas do |t| t.references :category, foreign_key: true t.text :body, null: false t.timestamps end end end ideasテーブルに、"t.references :category, foreign_key: true" とあるように、ideasテーブルは外部キーとしてcategoriesテーブルを参照しています。categoriesテーブルが親テーブル、ideasテーブルが子テーブルという関係です。 外部キー制約を設定する場合、外部キーの対応するデータが必ず存在しなくてはいけません。 外部キー制約とは、「他のテーブルのデータに参照(依存)するようにカラムに付ける制約」だからです。 ところが、今回のやり方だとcategoriesテーブルとideasテーブルを同時にmigrateしてしまい、本来なら先に作成されているべき親テーブルであるcategoriesテーブルが存在しない、といったエラーが出ていたのです。 解決方法 rails d model ideaコマンドで、ideaモデルとテーブルを一旦削除。 その後、rails db:migrateを実行し、まずはcategoriesテーブルを作成します。 そして、ideasモデルとテーブルを再度作成し、rails db:migrateを実行するとideasテーブルも無事に作成できました。 まとめ 外部キー制約は、親テーブルに存在しないデータを子テーブルに登録させないためのツールですので、テーブルを作成する時にも、まずは親テーブルを作成してから子テーブルの作成をするように気をつけたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

migrateに何度か失敗したあと、勝手にmigirationファイルができたと思い込みファイルを削除してしまった

migrationファイルを削除する経緯 sqlにきちんと命名してカラムを作成しようとしていました。 名前の読みがなカラムを「last_name_pronounciation」と命名してテーブルを作ろうとすると terminal Mysql2::Error: Specified key was too long; max key length is 767 bytes と怒られました。(名前が長すぎるようです) 何度か試しているうちに↑(上)矢印キーで過去に入力したコマンドを呼び出していたようで 勝手にmigrationファイルを作ってしまっていました。 terminal maedatakuo@maedatakudainoMacBook-Air furima-32844 % rails g devise user Running via Spring preloader in process 19160 invoke active_record create db/migrate/20210409083100_add_devise_to_users.rb File unchanged! The supplied flag value not found! app/models/user.rb 勝手にできているのだからと思い考えもせず追加migrationファイルを削除して、 続きでカラム名を考えながらテーブル構築していました(2度かdb:resetもしました) ようやく名前が決まって最終的にmigrateする段階でmigrationファイルをdownしようとrollbakcしたら terminal ================================ -- remove_index(:users, {:column=>:reset_password_token}) rails aborted! StandardError: An error has occurred, all later migrations canceled: migrationはキャンセルされましたとターミナルから言われました。 db:migrate:statusでmigrationのファイルの状態を調べてみると terminal database: furima_32844_development Status Migration ID Migration Name -------------------------------------------------- up 000 ********** NO FILE ********** up 20210409081355 Devise create users MigrationIDが書いてあれば仮ファイルを手作業で複製して通常通り手順に戻ってファイルを削除するのですが、 対処法 db:migrate:reset 再度テーブルをdelite、createし、migrationファイルも作成してくれるコマンドを実行して新しい名前のカラム作成に成功しました。 参考:削除ファイルのMigrationIDが残っていた場合 もし削除ファイルのMigrationIDが000でなくきちんと数値として残っていた場合の対処法 ①手作業でmigrationファイルを作成する ②通常の手順に戻ってファイルをrollbackし、ファイルをdownした状態で削除する ①のみ解説します。(②は通常の作業のため割愛) 1)db:migrate:statusを実行 2)削除された(NO FILE)ファイルのMigrationIDをコピー 3)db/migrateフォルダ内に 先ほどコピーしたMigrationIDを使って「20210409081355sample.rb」(*sampleは勝手に考えて入れています) を作成 4)ファイルの中身ですが MigrationID_sample.rb class Sample < ActiveRecord::Migration[6.0] def change end end をコピーしてお使いください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Useridがnilになったとき

環境 Rails 6.1.3.1 Ruby 3.0.0 MySQL 8.0.23 エラー内容 Rails consoleで新しいユーザーを作成したときのこと、 irb(main):001:0> User.create(name: "hogege",email: "hoge@hoge",password: "password",password_confirmation: "password") TRANSACTION (0.2ms) BEGIN User Exists? (0.7ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'hoge@hoge' LIMIT 1 TRANSACTION (0.2ms) ROLLBACK => #<User id: nil, name: "hogege", email: "hoge@hoge", created_at: nil, updated_at: nil, password_digest: [FILTERED]> Userid, created_at, update_atがnilになってしまいました +-----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------------+--------------+------+-----+---------+----------------+ | id | bigint | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | | email | varchar(255) | YES | UNI | NULL | | | created_at | datetime(6) | NO | | NULL | | | updated_at | datetime(6) | NO | | NULL | | | password_digest | varchar(255) | YES | | NULL | | +-----------------+--------------+------+-----+---------+----------------+ 確認してみても、idはauto_increaseになっているので、create!を使ってみる irb(main):002:0> User.create!(name: "hogege",email: "hoge@hoge",password: "password",password_confirmation: "password") TRANSACTION (6.7ms) BEGIN User Exists? (4.3ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'hoge2@hoge' LIMIT 1 TRANSACTION (1.9ms) ROLLBACK Traceback (most recent call last): 1: from (irb):7:in `<main>' ActiveRecord::RecordInvalid (Validation failed: Email is invalid) なんてことはない話でした。emailの形式が間違っていました、、、 irb(main):008:0> User.create(name: "hogege2",email: "hoge2@hoge.com",password: "password",password_confirmation: "password") TRANSACTION (0.2ms) BEGIN User Exists? (1.5ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'hoge2@hoge.com' LIMIT 1 User Create (1.5ms) INSERT INTO `users` (`name`, `email`, `created_at`, `updated_at`, `password_digest`) VALUES ('hogege2', 'hoge2@hoge.com', '2021-04-11 00:45:14.108385', '2021-04-11 00:45:14.108385', '$2a$12$g5Ks0L4Eiw.QotTeTzt2ieBsTNKBbrpJiNtb6C/JxERBqmus7UD7q') TRANSACTION (1.9ms) COMMIT => #<User id: 1, name: "hogege2", email: "hoge2@hoge.com", created_at: "2021-04-11 00:45:14.108385000 +0000", updated_at: "2021-04-11 00:45:14.108385000 +0000", password_digest: [FILTERED]> 無事成功! まとめ create 失敗しているときは何も教えてくれず終わり create! エラー内容を教えてくれる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActionTextを使うときにユーザー情報の関連づけで詰まった話

Rails6から出てきたActionTextというかんたんにブログ投稿ができるようになる機能があるのですが、これらを使うときにエラーになってしまったので共有します。 まずやりたかったこととしては、ユーザーの情報とこのブログの情報を関連づけさせたかったのですが、それらをするときにエラーになってしまいました。 しかし以下のようなエラーが出ました。 Mysql2::Error: Field 'user_id' doesn't have a default value というエラーが投稿ボタンを押して投稿しよようとしたときに出てきました。 どうしてかというとマイグレーションファイルにある記述に問題がありました。 今回結構参考になると思います。 まずActionTextとはなにか ActionTextとは何かというとかんたんにブログなどの長文のコンテンツを書くことができるものです。 全然違いますがはてなブログさんのようにブラウザ上でブログを書くことができます。 詳しい使い方はググってください、今回は私がどうしてミスったのかそこだけを解説していきます まずActionTextを使うとマイグレーションファイルが生成されます ・ ブログ投稿に関するマイグレーションファイル ・ActionTextに関するマイグレーションファイル 今回のActionTextには画像投稿ができるのできるようにするための ・Active Storageのマイグレーションファイル 以上の3つが生成されます。 そして今回何故ミスったかというと、ブログ投稿に関するマイグレーションファイルには t.references :user, null: false, foreign_key: true と記述してユーザーの情報を引っ張ってこようとしても何も問題はないのですが、ActionTextに関するマイグレーションファイルにその記述を書いてしまうとActionTextそのものにもユーザー情報が渡されないといけなくなってしまいます。 しかし今回投稿をする際にはブログ投稿の部分のカラムにはきちんと渡されるのですが、ActionTextの部分に関するテーブルにはユーザー情報行きません。 なので一番最初に出たようなエラーになってしまうわけです。 みなさんも是非気をつけてくだい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

転売のための商品大量購入防止アプリとしてTenba×IyaというWebアプリを作りました。

転売のための商品大量購入防止アプリとしてTenba×IyaというWebアプリを作りました。 なぜアプリを作ったかについて、転売問題の背景などと共に、僕の考えを説明します。 アプリの説明ページ https://tenba-iya.hatenablog.com/entry/about アプリへのリンク https://tenba-iya.herokuapp.com/ 転売問題の背景について 現状起こっていること ①PS5や任天堂スイッチのゲームにおいて、転売目的の商品の買い占めが起こっており、買い占められた商品が、メルカリなどのサービスで高額転売されている。 そのせいで、消費者が適正な価格で商品を購入できない。 ゲーム制作会社としては、本体は売れるが、買い占めにより、広く商品が流通しないため、ソフトが売れにくい状況が発生している。 PS5の転売についてのリンク https://www.itmedia.co.jp/news/articles/2101/26/news063.html ②日本酒の獺祭が転売目的で買い占められ、本来の価格で購入しにくくなっている。 獺祭の製造会社も新聞に、転売商品を買わないように新聞で呼びかけているが、効果が薄い。 獺祭の記事についてリンク https://www.tokyo-sports.co.jp/social/856267/ ③コンサートチケットが転売目的で購入され、高額で転売されている。 ファンは高くてもコンサートをみたいため、転売商品を購入してしまっている。 対策としては顔認証システムの導入などが進められてきているので、今後は改善されるかもしれない。 ④コロナ渦でマスクが買い占められ、一箱1万程度まで値上がりした。 必要な人が購入できなくなり、問題となった。 マスク買い占め記事のリンク https://www.tokyo-np.co.jp/article/14221 転売の問題点と対策 ①消費者が本来の価格で購入できなくなる →転売の商品を買わないようにすればいいが、どうしても必要な場合や、お金に余裕がある人は買ってしまう。 ②消費者は本来より高い価格で買わなければならなくなり、不利益を被る。転売屋は利益を得る。メルカリなどのプラットフォーム側も手数料で利益を得る。商品の元の販売者は利益は増えず、商品が売れにくくなる可能性がある。また、広く商品を届けたいときに届けれない事態となる。 →メルカリなどのプラットフォームは利益があるため、抜本的な対策を強要することは困難。 商品の元の販売者も、転売されない価格まで値段をあげればよいが、消費者に適切な価格で商品を届けたい思い(特にコンサートのチケットなどはファンを大切にしたいらしい)などから、難しい。 ③一部の人に商品が買い占められてしまい、必要な人に商品が届かない。 →同じ商品を一部の人に売らないように対策できると良いが、小売店舗ごとに誰がどの商品を買ったかを把握することは難しい。 以上の問題などについての解決策として、Webアプリを考えた。 特に上記の③の問題(一部の人に買い占められる問題)にフォーカスしてアプリのアイデアを考えた。 アプリのコンセプトなどもろもろについては以下のとおりです。 ①販売者、購入者共に手軽に使えるようにする。 レジの待ち時間や、アプリを使う手間が大きいと導入が難しいと思うので、なるべく簡単な方法とした。 具体的には、 ●購入者が電話番号を記入して、SMSでチケットを発行する方法とした。 ユーザー登録やパスワードの記入などはなくした。 アプリの存在を購入者が知らなくても、店側が電話番号を聞けば、運用できるような方法とした。 また、余計な登録がないので、個人情報なども漏れにくいと思います。 電話番号は、入力時以外は全部は見れないようにしています。 例:012-1234-1234の場合、入力時以外は***-****-**34と表示される。 ●Webアプリとすることで、スマホのアプリなど専用の物をインストールしなくて良いようにした。 ②購入者が店舗を変えると、今までにいくつ商品を買ったかわからなくなるので、店舗が変わってもわかるような仕組みを考えてみた。 商品毎にチケットを発行して、電話番号毎のそれぞれの商品購入数が判れば、対策が取れると考えて、みた。 例えば電話番号123-1234-1234でA店舗でマスクを1つ買った場合、このアプリを使えば、一つ買ったと記録が残ります。 次にB店舗に移動した場合も、登録する電話番号が同じならば、2つめを買っていることがわかります。 電話番号を変えた場合は、どうしようも有りませんが、そんなに電話番号を変えられるとは思っていません。 また、チケットにはチケット発行日、チケット使用日、当日、当月、総計の購入数がかいてあるので、どの程度同じ商品を買っているかがわかります。 販売者側は以上の情報をみて、販売するかどうかをその場で決める、もしくは、予め決めておけばいいなと考えています。 ③アプリの構成上として、商品の販売者側(生産者や小売店舗)が購入者側にアプリを使ってくださいというような、構成とした。 したがって、世間一般にアプリを広めるというより、企業側にアプリの存在を認識してもらうようなアプローチを今後しないといけないと考えています。 ④そもそも購入者(特に転売目的の購入者)はアプリを使うメリットは全くないので、企業側が使うようにお願いしないといけない。 ただ、転売目的でない、購入者はこのアプリが広まることで転売が少なくなるのならば、広める意味があるのかもしれない? ⑤そもそも、電話番号で登録すれば、重複した購入は制限できるのか? 僕は電話番号をたくさん持っている人はあまり知らないので、できると思っている。 ⑥アプリが広まってくれば、消費者側が予めチケットを用意して、レジでチケットあるよ。チケット使用済みにするよと、提示すれば、そこまで手間がかからずに運用できると思っています。 あくまで、このアプリが広まればですが。。。 ⑦一応ネット販売でも対応できるように、チケットIDを検索して、チケットを使えるような仕組みも用意しています。 使い方としては、購入者が電話番号でチケットを登録→チケットIDを販売者に連絡→販売者がチケットIDを検索してチケットを使用済みにする。です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails × Shopify Multipass APIでSSOを試してみる

概要 業務の一環でShopifyのMultipass APIを使いSSO(シングル・サイン・オン)を実装する機会があったので、手順などについてメモ書きしておきたいと思います。 SSO: 単一の資格情報(IDやメールアドレス)で複数のWebサービスにログインできる仕組みの事。 全体的な流れ Shopify側でMultipassを有効化しシークレットキーを発行。 SSO基盤側でシークレットキーをもとに暗号化キーと署名キーを抽出。さらに顧客情報(メールアドレス、ユーザーID、IPアドレス、名前、住所など)を暗号化したトークンを生成し、それらを含めたGETリクエストをShopify側へ送信。(GET: https://ストアのドメイン/account/login/multipass/トークン) 認証に成功するとマイページに遷移。 環境 Shopify Plus Docker Ruby 2.6 Rails 6 MySQL 8 まず大前提として、Shopify Plusプランに加入している必要があるのでご注意ください。(Multipass APIを利用できるのはShopify Plusのみなので) The Multipass login feature is available to Shopify Plus merchants only. https://shopify.dev/docs/admin-api/rest/reference/plus/multipass アプリケーション側の実装については、Dockerで簡単なRailsアプリを準備します。 下準備 まず、Shopify側でMultipass APIを有効化しなければなりません。 Shopify管理画面の左下から「設定」→「チェックアウト」と進み、「顧客アカウント」を任意もしくは必要とした上で「マルチパスを有効にする」をクリックします。 するとシークレットキーが表示されるはずなので、メモに控えておいてください。(後ほどアプリケーション側の実装を行う際に使用します。) 実装 次にアプリケーション側の実装に移ります。 ディレクトリを作成 $ mkdir shopify-multipass-api-on-rails $ cd shopify-multipass-api-on-rails 各種ファイルを作成 $ touch Dockerfile $ touch docker-compose.yml $ touch Gemfile $ touch Gemfile.lock ./Dockerfile FROM ruby:2.6.6 ENV LANG C.UTF-8 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ apt-get install nodejs RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH ADD Gemfile $APP_PATH/Gemfile ADD Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install ADD . $APP_PATH ./docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql ports: - 3306:3306 web: build: context: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - 3000:3000 depends_on: - db volumes: mysql-data: ./Gemfile source "https://rubygems.org" gem "rails", "~>6" /Gemfile.lock # 空欄でOK Railsプロジェクトを作成 $ docker-compose run web rails new . --force --no-deps --database=mysql --skip-test --webpacker Gemfileが更新されたので再度ビルド。 $ docker-compose build データベースを作成 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 「./config/database.yml」を変更し、データベースを作成。 $ docker-compose run web rails db:create 動作確認 コンテナを起動。 $ docker-compose up -d http://localhost3000 にアクセスしていつもの画面が表示されればOKです。 認証機能を作成 Deviseなどを使って新規登録/ログインを行うための認証機能を作成していきます。 ./Gemfile gem 'devise' Gemfileを更新したので再度ビルド。 $ docker-compose build Deviseをインストール $ docker-compose run web rails g devise:install ... create config/initializers/devise.rb create config/locales/devise.en.yml config以下のファイルが追加されたので、コンテナを再起動します。 $ docker-compose down $ docker-compose up -d modelを作成 $ docker-compose run web rails g devise user ... Running via Spring preloader in process 18 invoke active_record create db/migrate/20210410093331_devise_create_users.rb create app/models/user.rb insert app/models/user.rb route devise_for :users $ docker-compose run web rails db:migrate 各種controllerを作成 $ docker-compose run web rails g devise:controllers users ... Running via Spring preloader in process 18 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 動作確認 http://localhost:3000/users/sign_up にアクセスしして「Sigin up」ページが表示されれば成功です。 home_controllerを作成 $ docker-compose run web rails g controller home index ... Running via Spring preloader in process 18 create app/controllers/home_controller.rb route get 'home/index' invoke erb create app/views/home create app/views/home/index.html.erb invoke helper create app/helpers/home_helper.rb invoke assets invoke scss create app/assets/stylesheets/home.scss ./config/routes.rb Rails.application.routes.draw do root "home#index" devise_for :users end ./app/views/home/index.html.erb <h1>Home</h1> <% if user_signed_in? %> <p><%= link_to "Sign out", destroy_user_session_path, method: :delete %></p> <p><%= current_user.email %></p> <% else %> <p><%= link_to "Log in", new_user_session_path %> / <%= link_to "Sign up", new_user_registration_path %></p> <% end %> http://localhost:3000/ にアクセスして「Home」ページが表示されていればOKです。 試しに適当なメールアドレス・パスワードでユーザーを作成してみましょう。 ユーザー作成に成功すると「Home」ページに遷移するはず。メールアドレスが表示されている事からしっかりログイン状態になっているのも確認できますね。 Shopify Multipass APIを導入 最低限の認証機能が準備できたので、いよいよShopify Multipass APIを導入していきます。 ライブラリを作成 $ touch lib/shopify_multipass.rb ./lib/shopify_multipass.rb require "openssl" require "time" require "json" require "base64" class ShopifyMultipass attr_accessor :encryptionKey, :signingKey # 暗号化キーと署名キーを生成する def initialize(multipass_secret = nil) return if multipass_secret.blank? block_size = 16 hash = OpenSSL::Digest.new("sha256").digest(multipass_secret) self.encryptionKey = hash[0, block_size] self.signingKey = hash[block_size, 32] end # 顧客情報を暗号化してトークンを生成する def generate_token(customer_data_hash) return if !customer_data_hash customer_data_hash["created_at"] = Time.now.iso8601 cipherText = self.encrypt(customer_data_hash.to_json) Base64.urlsafe_encode64(cipherText + self.sign(cipherText)) end def encrypt(plaintext) cipher = OpenSSL::Cipher.new("aes-128-cbc") cipher.encrypt cipher.key = self.encryptionKey cipher.iv = iv = cipher.random_iv iv + cipher.update(plaintext) + cipher.final end def sign(data) OpenSSL::HMAC.digest("sha256", self.signingKey, data) end # Shopify側で認証を行うための顧客情報とトークンが入ったURLを生成する def generate_url(customer_data_hash, domain) return if !domain return "https://" + domain + "/account/login/multipass/" + self.generate_token(customer_data_hash) end end lib/以下を読み込むために、「./config/application.rb」に次の1行を追加します。 ./config/application.rb config.autoload_paths += %W(#{config.root}/lib) config以下のファイルが追加されたので、コンテナを再起動します。 $ docker-compose down $ docker-compose up -d shopify_multipass_controllerを作成 $ docker-compose run web rails g controller shopify_multipass confirm login .... Running via Spring preloader in process 18 create app/controllers/shopify_multipass_controller.rb route get 'shopify_multipass/confirm' get 'shopify_multipass/login' invoke erb create app/views/shopify_multipass create app/views/shopify_multipass/confirm.html.erb create app/views/shopify_multipass/login.html.erb invoke helper create app/helpers/shopify_multipass_helper.rb invoke assets invoke scss create app/assets/stylesheets/shopify_multipass.scss ./app/controllers/shopify_multipass_controller.rb class ShopifyMultipassController < ApplicationController def confirm end def login # Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意) customer_data = { email: current_user.email, identifier: current_user.id } shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー") # 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」 url = shopify_multipass.generate_url(customer_data, "ストアのドメイン") redirect_to url end end customer_dataに含める事ができる値 email: メールアドレス first_name: 名 last_name: 姓 tag_string: タグ identifer: UID return_to: 認証後の遷移先(何も指定しない場合は「ストアのドメイン/account」ページに飛ぶ) remort_ip: IPアドレス addresses: 届け先住所 ※メールアドレスのみ必須項目でそれ以外は任意。(今回の例ではユーザーIDをidentiferとして渡しています。) より詳細な情報は公式ドキュメントを参照。 https://shopify.dev/docs/admin-api/rest/reference/plus/multipass ./app/views/shopify_multipass/confirm.html.erb <h1>ShopifyMultipass</h1> <p><%= link_to "Multipass login", login_multipass_redirect_path %></p> ./config/routes.rb authenticate :user do get "login/multipass", to: "shopify_multipass#confirm" get "login/multipass/redirect", to: "shopify_multipass#login" end 「authenticate :user do ~ end」で囲み、ログイン済みのユーザーだけがアクセスできるようにしておきます。 動作確認 http://localhost:3000//login/multipass にアクセスし、「Multipass login」をクリックしてみましょう。 上手くいくとマイページ(ストアのドメイン/account)に遷移するはずです。 Shopify管理画面からもMultipass API経由でユーザーがログインされている事が確認できました。 Tips これで最小限の実装は完了しましたが、実運用を想定した場合、個人的に気をつけた方が良いと思う点がいくつかあるので記述しておきます。 メールアドレスの整合性について 何よりもまず気になるのがShopify ⇄ SSO基盤間におけるメールアドレスの整合性です。 というのも、両者は別にデータベースを共有しているわけではないため、たとえばSSO基盤側のメールアドレスを変更した際、同時にShopify側のメールアドレスも変更される仕組みを作っておかないと整合性が取れなくなり、全く別のユーザーとしてログインする事になってしまいます。(Shopifyではメールアドレスをユニーク識別子としているため。) 主な解決方法としては、Shopifyが提供しているCustomer APIを使うのが良さげです。 https://shopify.dev/docs/admin-api/rest/reference/customers/customer 実際、自分が携わっているサービスでは、メールアドレス変更時に上記APIを叩いてShopify側が保持しているメールアドレスも変更する事で整合性を保つようにしています。 ログイン後の遷移先について Multipass APIを利用したログイン後の遷移先を「return_to」で指定できるというのは先述した通りです。 何も指定していないデフォルトの状態だとマイページに飛ぶようになっていますが、良くあるような「ショッピングカート」→「ログイン」→「ショッピングカート(に戻る)」といったフローを実現したい場合はクエリパラメータなどで遷移先のURLを上手く拾ってあげる必要があります。 たとえば、 http://localhost:3000/login/multipass?redirect_uri=ショッピングカートのURL といった感じで、Shopify → SSO基盤への遷移時にクエリパラメータとしてショッピングカートのURLを渡しておけば、あとは ./app/controllers/shopify_multipass_controller.rb class ShopifyMultipassController < ApplicationController def confirm end def login # Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意) customer_data = { email: user.email, identifier: user.id, } # クエリパラメータ「redirect_uri」が含まれていた場合は拾う。 uri = URI(request.referer) if request.referer if uri && uri.query.present? q_array = URI::decode_www_form(uri.query) q_hash = Hash[q_array] customer_data["return_to"] = q_hash["redirect_uri"] if q_hash["redirect_uri"].present? end shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー") # 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」 url = shopify_multipass.generate_url(customer_data, "ストアのドメイン") redirect_to url end end 「request.referer」を使う事でその値を「return_to」内に含める事ができます。 あとがき 以上、Shopify Multipass APIを使ったSSOを試してみました。今回紹介したコードはあくまでサンプルなので、実運用を想定した場合は何かと不備があるかもしれませんが、その辺はご了承ください。 Tipsでも取り上げているように、色々と工夫しなければならない点はあるので、ご自身のプロジェクトの仕様などを踏まえた上で試行錯誤していただければと思います。 今回作成したアプリのソースコード: https://github.com/kazama1209/shopify-multipass-api-on-rails
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails × Shopify Multipass APIでSSO(シングル・サイン・オン)を試してみる

概要 業務の一環でShopifyのMultipass APIを使いSSO(シングル・サイン・オン)を実装する機会があったので、手順などについてメモ書きしておきたいと思います。 SSO: 単一の資格情報(IDやメールアドレス)で複数のWebサービスにログインできる仕組みの事。 全体的な流れ Shopify側でMultipassを有効化しシークレットキーを発行。 SSO基盤側でシークレットキーをもとに暗号化キーと署名キーを抽出。さらに顧客情報(メールアドレス、ユーザーID、IPアドレス、名前、住所など)を暗号化したトークンを生成し、それらを含めたGETリクエストをShopify側へ送信。(GET: https://ストアのドメイン/account/login/multipass/トークン) 認証に成功するとマイページに遷移。 環境 Shopify Plus Docker Ruby 2.6 Rails 6 MySQL 8 まず大前提として、Shopify Plusプランに加入している必要があるのでご注意ください。(Multipass APIを利用できるのはShopify Plusのみなので) The Multipass login feature is available to Shopify Plus merchants only. https://shopify.dev/docs/admin-api/rest/reference/plus/multipass アプリケーション側の実装については、Dockerで簡単なRailsアプリを準備します。 下準備 まず、Shopify側でMultipass APIを有効化しなければなりません。 Shopify管理画面の左下から「設定」→「チェックアウト」と進み、「顧客アカウント」を任意もしくは必要とした上で「マルチパスを有効にする」をクリックします。 するとシークレットキーが表示されるはずなので、メモに控えておいてください。(後ほどアプリケーション側の実装を行う際に使用します。) 実装 次にアプリケーション側の実装に移ります。 ディレクトリを作成 $ mkdir shopify-multipass-api-on-rails $ cd shopify-multipass-api-on-rails 各種ファイルを作成 $ touch Dockerfile $ touch docker-compose.yml $ touch Gemfile $ touch Gemfile.lock ./Dockerfile FROM ruby:2.6.6 ENV LANG C.UTF-8 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ apt-get install nodejs RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH ADD Gemfile $APP_PATH/Gemfile ADD Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install ADD . $APP_PATH ./docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql ports: - 3306:3306 web: build: context: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp - ./vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - 3000:3000 depends_on: - db volumes: mysql-data: ./Gemfile source "https://rubygems.org" gem "rails", "~>6" /Gemfile.lock # 空欄でOK Railsプロジェクトを作成 $ docker-compose run web rails new . --force --no-deps --database=mysql --skip-test --webpacker Gemfileが更新されたので再度ビルド。 $ docker-compose build データベースを作成 ./config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているはずなので変更 host: db # デフォルトだとlocalhostになっているはずなので変更 「./config/database.yml」を変更し、データベースを作成。 $ docker-compose run web rails db:create 動作確認 コンテナを起動。 $ docker-compose up -d http://localhost3000 にアクセスしていつもの画面が表示されればOKです。 認証機能を作成 Deviseなどを使って新規登録/ログインを行うための認証機能を作成していきます。 ./Gemfile gem 'devise' Gemfileを更新したので再度ビルド。 $ docker-compose build Deviseをインストール $ docker-compose run web rails g devise:install ... create config/initializers/devise.rb create config/locales/devise.en.yml config以下のファイルが追加されたので、コンテナを再起動します。 $ docker-compose down $ docker-compose up -d modelを作成 $ docker-compose run web rails g devise user ... Running via Spring preloader in process 18 invoke active_record create db/migrate/20210410093331_devise_create_users.rb create app/models/user.rb insert app/models/user.rb route devise_for :users $ docker-compose run web rails db:migrate 各種controllerを作成 $ docker-compose run web rails g devise:controllers users ... Running via Spring preloader in process 18 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 動作確認 http://localhost:3000/users/sign_up にアクセスしして「Sigin up」ページが表示されれば成功です。 home_controllerを作成 $ docker-compose run web rails g controller home index ... Running via Spring preloader in process 18 create app/controllers/home_controller.rb route get 'home/index' invoke erb create app/views/home create app/views/home/index.html.erb invoke helper create app/helpers/home_helper.rb invoke assets invoke scss create app/assets/stylesheets/home.scss ./config/routes.rb Rails.application.routes.draw do root "home#index" devise_for :users end ./app/views/home/index.html.erb <h1>Home</h1> <% if user_signed_in? %> <p><%= link_to "Sign out", destroy_user_session_path, method: :delete %></p> <p><%= current_user.email %></p> <% else %> <p><%= link_to "Log in", new_user_session_path %> / <%= link_to "Sign up", new_user_registration_path %></p> <% end %> http://localhost:3000/ にアクセスして「Home」ページが表示されていればOKです。 試しに適当なメールアドレス・パスワードでユーザーを作成してみましょう。 ユーザー作成に成功すると「Home」ページに遷移するはず。メールアドレスが表示されている事からしっかりログイン状態になっているのも確認できますね。 Shopify Multipass APIを導入 最低限の認証機能が準備できたので、いよいよShopify Multipass APIを導入していきます。 ライブラリを作成 $ touch lib/shopify_multipass.rb ./lib/shopify_multipass.rb require "openssl" require "time" require "json" require "base64" class ShopifyMultipass attr_accessor :encryptionKey, :signingKey # 暗号化キーと署名キーを生成する def initialize(multipass_secret = nil) return if multipass_secret.blank? block_size = 16 hash = OpenSSL::Digest.new("sha256").digest(multipass_secret) self.encryptionKey = hash[0, block_size] self.signingKey = hash[block_size, 32] end # 顧客情報を暗号化してトークンを生成する def generate_token(customer_data_hash) return if !customer_data_hash customer_data_hash["created_at"] = Time.now.iso8601 cipherText = self.encrypt(customer_data_hash.to_json) Base64.urlsafe_encode64(cipherText + self.sign(cipherText)) end def encrypt(plaintext) cipher = OpenSSL::Cipher.new("aes-128-cbc") cipher.encrypt cipher.key = self.encryptionKey cipher.iv = iv = cipher.random_iv iv + cipher.update(plaintext) + cipher.final end def sign(data) OpenSSL::HMAC.digest("sha256", self.signingKey, data) end # Shopify側で認証を行うための顧客情報とトークンが入ったURLを生成する def generate_url(customer_data_hash, domain) return if !domain return "https://" + domain + "/account/login/multipass/" + self.generate_token(customer_data_hash) end end lib/以下を読み込むために、「./config/application.rb」に次の1行を追加します。 ./config/application.rb config.autoload_paths += %W(#{config.root}/lib) config以下のファイルが追加されたので、コンテナを再起動します。 $ docker-compose down $ docker-compose up -d shopify_multipass_controllerを作成 $ docker-compose run web rails g controller shopify_multipass confirm login .... Running via Spring preloader in process 18 create app/controllers/shopify_multipass_controller.rb route get 'shopify_multipass/confirm' get 'shopify_multipass/login' invoke erb create app/views/shopify_multipass create app/views/shopify_multipass/confirm.html.erb create app/views/shopify_multipass/login.html.erb invoke helper create app/helpers/shopify_multipass_helper.rb invoke assets invoke scss create app/assets/stylesheets/shopify_multipass.scss ./app/controllers/shopify_multipass_controller.rb class ShopifyMultipassController < ApplicationController def confirm end def login # Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意) customer_data = { email: current_user.email, identifier: current_user.id } shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー") # 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」 url = shopify_multipass.generate_url(customer_data, "ストアのドメイン") redirect_to url end end customer_dataに含める事ができる値 email: メールアドレス first_name: 名 last_name: 姓 tag_string: タグ identifer: UID return_to: 認証後の遷移先(何も指定しない場合は「ストアのドメイン/account」ページに飛ぶ) remort_ip: IPアドレス addresses: 届け先住所 ※メールアドレスのみ必須項目でそれ以外は任意。(今回の例ではユーザーIDをidentiferとして渡しています。) より詳細な情報は公式ドキュメントを参照。 https://shopify.dev/docs/admin-api/rest/reference/plus/multipass ./app/views/shopify_multipass/confirm.html.erb <h1>ShopifyMultipass</h1> <p><%= link_to "Multipass login", login_multipass_redirect_path %></p> ./config/routes.rb authenticate :user do get "login/multipass", to: "shopify_multipass#confirm" get "login/multipass/redirect", to: "shopify_multipass#login" end 「authenticate :user do ~ end」で囲み、ログイン済みのユーザーだけがアクセスできるようにしておきます。 動作確認 http://localhost:3000//login/multipass にアクセスし、「Multipass login」をクリックしてみましょう。 上手くいくとマイページ(ストアのドメイン/account)に遷移するはずです。 Shopify管理画面からもMultipass API経由でユーザーがログインされている事が確認できました。 Tips これで最小限の実装は完了しましたが、実運用を想定した場合、個人的に気をつけた方が良いと思う点がいくつかあるので記述しておきます。 メールアドレスの整合性について 何よりもまず気になるのがShopify ⇄ SSO基盤間におけるメールアドレスの整合性です。 というのも、両者は別にデータベースを共有しているわけではないため、たとえばSSO基盤側のメールアドレスを変更した際、同時にShopify側のメールアドレスも変更される仕組みを作っておかないと整合性が取れなくなり、全く別のユーザーとしてログインする事になってしまいます。(Shopifyではメールアドレスをユニーク識別子としているため。) 主な解決方法としては、Shopifyが提供しているCustomer APIを使うのが良さげです。 https://shopify.dev/docs/admin-api/rest/reference/customers/customer 実際、自分が携わっているサービスでは、メールアドレス変更時に上記APIを叩いてShopify側が保持しているメールアドレスも変更する事で整合性を保つようにしています。 ログイン後の遷移先について Multipass APIを利用したログイン後の遷移先を「return_to」で指定できるというのは先述した通りです。 何も指定していないデフォルトの状態だとマイページに飛ぶようになっていますが、良くあるような「ショッピングカート」→「ログイン」→「ショッピングカート(に戻る)」といったフローを実現したい場合はクエリパラメータなどで遷移先のURLを上手く拾ってあげる必要があります。 たとえば、 http://localhost:3000/login/multipass?redirect_uri=ショッピングカートのURL といった感じで、Shopify → SSO基盤への遷移時にクエリパラメータとしてショッピングカートのURLを渡しておけば、あとは ./app/controllers/shopify_multipass_controller.rb class ShopifyMultipassController < ApplicationController def confirm end def login # Shopifyへ渡す顧客情報(必須項目はメールアドレスでそれ以外は任意) customer_data = { email: user.email, identifier: user.id, } # クエリパラメータ「redirect_uri」が含まれていた場合は拾う。 uri = URI(request.referer) if request.referer if uri && uri.query.present? q_array = URI::decode_www_form(uri.query) q_hash = Hash[q_array] customer_data["return_to"] = q_hash["redirect_uri"] if q_hash["redirect_uri"].present? end shopify_multipass = ShopifyMultipass.new("マルチパスのシークレットキー") # 次のようなURLが作成される 「https://<ストアのドメイン>/account/login/multipass/<トークン>」 url = shopify_multipass.generate_url(customer_data, "ストアのドメイン") redirect_to url end end 「request.referer」を使う事でその値を「return_to」内に含める事ができます。 あとがき 以上、Shopify Multipass APIを使ったSSOを試してみました。今回紹介したコードはあくまでサンプルなので、実運用を想定した場合は何かと不備があるかもしれませんが、その辺はご了承ください。 Tipsでも取り上げているように、色々と工夫しなければならない点はあるので、ご自身のプロジェクトの仕様などを踏まえた上で試行錯誤していただければと思います。 今回作成したアプリのソースコード: https://github.com/kazama1209/shopify-multipass-api-on-rails
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む