20210411のRubyに関する記事は20件です。

【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で続きを読む

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で続きを読む

aa

aa
  • このエントリーをはてなブックマークに追加
  • 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の数値の標準入力方法

Rubyの数値の標準入力方法です。 number = gets.chomp.to_i chompは改行コードを取り払う関数です。 to_iは文字列を数値に変換する関数です。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

リレー式論理回路シミュレータを自作して1bit CPUまで動かした

「1bit CPU」というのは、書籍『CPUの創りかた』に載っている「オリジナルCPU 試作3号機(エレキ式)」のことです。 (※ ブログに書いた記事を Qiita に引っ越してきました。元記事を書いたのは 2020-05-03 です。) 概要 リポジトリ: デモ(ブラウザで動かせます): https://sonota88.github.io/kairo-gokko/pages/39/index.html 音量を小さめにしていますが、音が出ます 表示サイズや負荷の関係でPCブラウザ推奨です とりあえず富豪的に作っていて、まだ最適化などやってません 最初は発振するので、スイッチを適当に操作して発振を止める必要があります。 操作についてはこちらも参照してください。 製作過程のメモ(どういうステップを踏んで何を実装していったか、具体的な話はこっちにまとめています): 動かしている様子です。 左下のスイッチがクロックで、右下のスイッチで命令( MOV A, A と NOT A )を切り替えています。 回路図を描いて動かすまでの流れ: 回路図を LibreOffice Draw で描いて .fodg 形式で保存 fodg ファイルを読んで前処理し、中間データ(JSON)に変換 シミュレータのプログラムと中間データを使ってブラウザで実行 LibreOffice Draw で描いた回路図はこんな感じ。 直線と矩形、矩形内のテキストだけです。部品はグリッドに合わせて配置します。ナナメの配線は禁止。 使っている部品: 導線 電池のプラス極・マイナス極 ランプ スイッチ。マウスでクリックすると ON/OFF が切り替わります。 リレー(2種類) ランプ、リレーには極性なし(プラス極・マイナス極を入れ替えても動作は変わらない) 小さい回路の例をいくつか: NOT(を3つ繋げたもの) NAND XOR RSフリップフロップ ほぼ Ruby 製。ブラウザで動かす部分は DXOpal を使っています。 (一応残してあるが)不要になっている部分、補助ツール的なスクリプト、コメントなどを除くと主な部分は 2000行弱(2021-04-11 時点)。途中いろいろありましたができあがってみると意外と小さい。 $ wc -l *.rb 283 child_circuit.rb 456 circuit.rb 114 drawer_dxopal.rb 137 libo_draw.rb 237 main.rb 26 preprocess.rb 307 unit.rb 346 view.rb 1906 合計 期間 2019-12-31 〜 2020-02-02 (プロトタイプを作り始めてから 1bit CPU が動くまで) 始める前からいろいろ調べたりはしていて、書き始めたのが 12/31 だったか 12/30 だったか。 どのくらいの電気の知識で始めたか 小学校、中学校の理科レベル 電池、エナメル線、豆電球、電磁石、直列・並列つなぎ 大学の頃にちょっとだけ電子工作やろうとするも鉱石ラジオ作った程度で終わった 『CPUの創りかた』のうっすらした記憶 リレーは名前は聞いたことあったけどどういうものか知らなかった トランジスタの前は真空管を使っていたらしい GND って何? ……という程度。 コンセプト・方針 ガチャガチャ動く機械仕掛けのおもちゃ ピタゴラ装置みたいなの 電圧や抵抗は考慮しない 難しそうなので 電気が流れているか、流れていないかだけ考える 小学校の理科の工作の延長程度のもの なるべく寄り道せずに曳光弾を通す 遅すぎて辛い、ということがなければ最適化は後回し そもそも作れるのか、(論理)回路を動かすには最低限何が必要なのかなど、 よく分かっていなかった なぜ作ったか / 作り始めるまでの経緯 並行してボンヤリと考えていたのですっきりまとめることができませんが、思い出して雑に書き出してみました。 列挙の順序も適当です。 そもそもは『CPUの創りかた』 最初に読んだのは 4年くらい前 かなり基本的なところから手取り足取り説明されていてとっても良い本だが、 単に読むだけだと後半で分からなくなる さすがに何も知らない私みたいなド素人が さらっと読んだだけで理解できるほど甘くはない (自分のような初心者は)作りながら読まないと分からないのでは 回路図を読むところがネックになる 回路図というものは抽象化された図なわけで、 そこから具体的な回路や動作がどうなるかよく分からない しかし作るのは面倒 部品の調達 また部屋が散らかる…… 知識・経験がなさすぎてゴールが遠い 物理CPU作るのは老後の楽しみにしよう…… とはいえ、先にしくみだけでも理解したい しくみを理解するには、やはり自分で作って電子回路についての経験を積む必要があるんだろうな…… → 「しかし作るのは面倒」に戻る ブートストラップ問題 しくみを理解するだけなら、枝葉の部分を捨ててもっと簡素化してハードルを下げられないか こういうのって、実際に手を動かして作って壊して、回路図とにらめっこしたり計算をやりなおして試行錯誤を繰り返すうちにだんだん勘が養われていくものだと思いますが、そういう経験がない状態なのでなかなか大変です。 論理回路シミュレータ そもそも論理回路シミュレータというものをよく知らなかった シミュレータで CPU(TD4など)を作っている人たちがいると知る 物理CPU作る前にこれで試すのは良さそう 手間・時間をかけずに好きなだけスクラップ&ビルドできる 部屋も散らからない nand2tetlis 通称(?) nand2tetlis 物理的な電子回路でなぜ論理回路が動くのか、という部分がよく分からないまま HDL に進んでしまうのがちょっと不満 そこの部分のブラックボックス感を解消したいのに そもそも論理回路が動くしくみがよく分かってない 入力側から電流が流れてきて、それが出力側に伝わるというのは分かる NOTゲートってどうなってるの。謎。 入力が L で出力が H になる場合、その H の電力はどこから来てるの 省略されると分からない フリップフロップ タイミングが絡んでくるし、入力がループするので、 脳内でエミュレートできない (自分で作って)動かしてみないとどうも実感が湧かない 論理回路図というものは抽象化された図なわけで、 どうすればそれを物理的な回路で実現できるかが分からない 今だからこうやって言語化できていますが、もっとボンヤリとモヤモヤしてました。「なんとなく……分かった気がする……たぶん……とりあえずそういうものだと飲み込んで先に進んでもいいが……(モヤモヤ)」みたいな。 リレー トランジスタについてもよく分かっていなかったので、 トランジスタ → 真空管 → リレー と遡って調べたりしていた リレーについて知ることで 「要するに何をやっているのか」「何のための部品なのか」が分かった 要するに「電気を使ってスイッチを切り替える装置」 英語版 Wikipedia の説明が簡潔で良いです。 "A relay is an electrically operated switch." 「それを早く教えてよ!」という気分に リレー、めっちゃ良い。分かりやすい。小学生でも理解できる。電磁石まで知ってればあともう一歩。そのくせインパクトがでかい。 「小学校の理科でついでにリレーまで教えてくれてればよかったのに!」 という気分に 1 「小学校の理科の知識に、あとはリレーだけ付け加えれば CPU まで作れる」 というのは、なんかいい。よくないですか? トランジスタについても、「N型 と P型があって正孔が……」という 半導体自体の説明よりも、 先に役割や目的について着目した方が(自分には)分かりやすかったと思います たしか、ここがクリアされたことで「あ、これなら自分でも作れるかも?」みたいな気分になった気がします。 Minecraft や Oxygen Not Included の論理回路 楽しい あの要素だけなら、自分でもなんとか作って遊べるのではないか? そう思わせてくれるところがとても良い。教育的。 グリッド配置にしたのはこれらの影響 他にも、マリオメーカーや Cities: Skylines などで作ってみた系の記事や動画を見て 「その部分だけ抜き出したもの」程度なら自分でも作れないか、 と妄想したり (2010)スゴすぎ! 「Minecraft」で本物の16ビットコンピュータを再現 - ねとらぼ (2015)【論理演算】マリオメーカーに「3+3=6」を計算させてみた - ニコニコ動画 (2019)『Cities: Skylines』でうんちを利用した計算機が制作される。汚水が流れて4ビットの回路を動かす | AUTOMATON 水流モデル(失敗) 電気について詳しくないので水流モデルの方が分かりやすいかな、と思って セルオートマトンで作ろうとした 逆に複雑で難しかったので中止 どうやって作るか ググっても「論理回路シミュレータの作りかた」なんて解説は出てこない 自分のイメージに一番近かったのがこれ: A puzzle game that makes us build a complete computer - Joscha Bach OSS なシミュレータのソースを読む手もあるが…… Logisim とか 2 そんなに本格的なものでなくてよい おもちゃみたいなのでよい 実用目的ではない。いわゆる「教育目的」なもの。 枝葉を排して理解しやすさを優先したもの。 自分が作りたかったのは普通の論理回路シミュレータではなく、 電池と導線と……で論理回路が動かせる、抽象化されていないシミュレータ 結局手探りで 詳しくは 製作過程のメモ の方に書きました 感想 おもしろかった!! 物理回路と論理回路との接続の部分が分かってよかった 1bit CPU を自分で実際に作れてよかった 生まれて初めて(シミュレータ上だけど)CPU を自作した! めでたい! NOTゲートえらい NOTゲートが作れるようになるのでリレーは偉大な発明 で、リレーで論理回路を作るアイデアはどこから出てきたんだ、誰が始めたんだ…… と調べていくとクロード・シャノンにたどり着きます。 コンピュータの黎明期の話というとチューリングやノイマンが有名ですが、 シャノンも非常に重要な仕事をしていたんですね (これも今回いろいろ調べていて知りました)。 連載:インターネット・サイエンスの歴史人物館(2)クロード・シャノン | 情報通信(ICT) | スマートグリッドフォーラム プログラミングの10大基礎知識(3) | 日経クロステック(xTECH) 電気の知識がちょっと増えてよかった ふだんやらないことをやると知らないことだらけで楽しい むちゃくちゃ難易度高いということもなく、動くと楽しいのでプログラミングのお題としてもおもしろいと思います。おすすめ。 TODO 気が向いたらやりたい 最適化・リファクタリング 現状ではだいぶ富豪的なので タイミングチャート付ける そんなに難しくなさそう (2020-07-26) 付けました 1bit CPU から先 これ以上規模が大きくなると大変そうだし、 論理回路と物理回路の接続の理解についてはだいぶ気が済んだので、 続きを作るとしたら既存の論理回路シミュレータを使えばよさそう オブザーバーパターンや Actor モデルを使って書いてみる? 関連 今のところ直接関連するというわけではないですが、もっと上のレイヤーの話ということで。1bit CPU と下記の VM の間のギャップも埋めていければなあと考えてはいます。 RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話 Rubyで素朴な自作言語のコンパイラを作った 作った後に読んだもの 【東京書籍】 一般書籍 文芸 コンピュータ、どうやってつくったんですか? 2020-12 に読んだ。リレーで説明されてて良い。 作ろう!CPU | マイナビブックス 指導要領の都合とかでいろいろあるんだと思います ↩ 有名っぽいので Logisim を例に出しましたが、Logisim は開発が終わっているようなので他のものを探すのが良さそうです ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlutterでCocoaPods not installed.と言われたら

versions % sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H524 android studio 4.1.3 xcode 12.4 % ruby -v ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19] 手動で、xcodeやandroidstudioを入れる。flutterもインストールした状態。 おもむろにflutter doctorしたら、なんかxcodeでwarning? してる。 結論 CocoaPodsをhomebrewで入れるとよいようだ。 % flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 2.0.4, on Mac OS X 10.15.7 19H524 darwin-x64, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) [!] Xcode - develop for iOS and macOS ✗ CocoaPods not installed. CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side. Without CocoaPods, plugins will not work on iOS or macOS. For more info, see https://flutter.dev/platform-plugins To install see https://guides.cocoapods.org/using/getting-started.html#installation for instructions. [✓] Chrome - develop for the web [✓] Android Studio (version 4.1) [✓] Connected device (2 available) とりあえず、インストールする。 エラー。 % sudo gem install cocoapods Building native extensions. This could take a while... ERROR: Error installing cocoapods: ERROR: Failed to build gem native extension. current directory: /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.0/ext/ffi_c /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby -I /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0 -r ./siteconf20210411-38038-1sym66t.rb extconf.rb checking for ffi.h... *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --without-opt-dir --with-opt-include --without-opt-include=${opt-dir}/include --with-opt-lib --without-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/$(RUBY_BASE_NAME) --with-ffi_c-dir --without-ffi_c-dir --with-ffi_c-include --without-ffi_c-include=${ffi_c-dir}/include --with-ffi_c-lib --without-ffi_c-lib=${ffi_c-dir}/lib --enable-system-libffi --disable-system-libffi --with-libffi-config --without-libffi-config --with-pkg-config --without-pkg-config /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:467:in `try_do': The compiler failed to generate an executable file. (RuntimeError) You have to install development tools first. from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `block in try_compile' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:534:in `with_werror' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:585:in `try_compile' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1109:in `block in have_header' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:959:in `block in checking_for' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block (2 levels) in postpone' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:361:in `block in postpone' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:331:in `open' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:357:in `postpone' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:958:in `checking_for' from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/mkmf.rb:1108:in `have_header' from extconf.rb:10:in `system_libffi_usable?' from extconf.rb:42:in `<main>' To see why this extension failed to compile, please check the mkmf.log which can be found here: /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-19/2.6.0/ffi-1.15.0/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.0 for inspection. Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-19/2.6.0/ffi-1.15.0/gem_make.out 途方にくれる。。。 いろいろやって、rubyのバージョンが違うのかと入れ直すなどする。 結果、こちらを参考にbrewで入れる。 macos - How to update cocoapods for flutter on Mac without getting an error? - Stack Overflow % brew install cocoapods Updating Homebrew... ==> Auto-updated Homebrew! Updated 1 tap (homebrew/core). ==> Updated Formulae Updated 2 formulae. Warning: Treating cocoapods as a formula. For the cask, use homebrew/cask/cocoapods Warning: cocoapods 1.10.1 is already installed, it's just not linked. To link this version, run: brew link cocoapods すでにインストールしていたのでlinkを貼る % brew link cocoapods Linking /usr/local/Cellar/cocoapods/1.10.1... Error: Could not symlink bin/xcodeproj Target /usr/local/bin/xcodeproj already exists. You may want to remove it: rm '/usr/local/bin/xcodeproj' To force the link and overwrite all conflicting files: brew link --overwrite cocoapods To list all files that would be deleted: brew link --overwrite --dry-run cocoapods うまくいかなかったので、上書きをする。 % brew link --overwrite cocoapods Linking /usr/local/Cellar/cocoapods/1.10.1... 2 symlinks created. % flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 2.0.4, on Mac OS X 10.15.7 19H524 darwin-x64, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio (version 4.1) [✓] Connected device (1 available) • No issues found! できた!長かった。途中で諦めようかと思った。 参考にさせていただきました Flutterの環境構築(Mac編)|KBOYのFlutter大学【基礎編】 macOS install - Flutter CocoaPods Guides - Getting Started macOS バージョンの確認方法 - PC設定のカルマ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

素数個の点を持つ楕円曲線を生成するCM法の実装

プログラムの全体はこちら。 https://github.com/anang0g0/ecc/blob/master/cm.rb 楕円曲線の離散対数問題は量子計算機に弱いので、もうオワコンなのだが意外なところで復活の兆しを見せている。 それは同種写像と呼ばれているものと関係がある。同種写像なんていうといかにも難しそうに聞こえるが、同じ個数の点を持つ楕円曲線同士を対応付ける写像のことである。 点の数が同じというだけで同種写像が存在するのであればかなり楽ではある。 ウィキにある通り、j不変料が同じ楕円曲線のことを同種と呼ぶらしい。 直感的には点の個数が同じ方がイメージしやすいのだが私の認識が間違っているようだ。 この暗号系は既存のリソースが活かせるので、楕円曲線暗号をやっていた人がPQCに乗り換えるとしたら一番の近道なのではないかと思う。ここで言う超特異というのは、楕円曲線のj-不変量と呼ばれているもので、この値が0になる楕円曲線を超特異とよぶ。 たしかそう書いてあったような気がするが、自信がない。 もしそうなら、j不変量が0になる虚数乗法をもつ楕円曲線は判別式D=3の場合に限られる。 そこで何が必要かといえば、同じ数の点を持つ楕円曲線をどうやって見つけるかだ。 その方法の一つがCM法と言われている方法だ。 しかし、単に点の数が同じであれば何でもいいのか詳しく検討していない。ウィキによると既に素体上ではなく二次拡大を使っているようなので、単なる素体では実現できないのかもしれない。その場合はCM法を二次拡大に対応できるように改造する必要がある。 ここに上げる実装では素数個の点をもつ楕円曲線に限られているが、素数を固定した上で楕円曲線の係数をかえて並列計算してやれば同じ数の点を持つ曲線が見つかるかもしれない。 CMというのは虚数乗法と言われる数学からの物体Xであり、暗号をやっていく上で何が出てくるか予測の出来ない手強い理論の一つである。ちょっと参考文献を探してみたのだが適当なのがネットにないので、楕円曲線暗号というベタなタイトルの書籍を紹介する。 以下にサンプルソースを上げておく。 同種写像暗号に使えるかどうかはまだ研究途上なのでよくわからないが、オリジナルのアプリに組み込むための独自の楕円曲線を生成するのに使えるのではないだろうか。 def alp() h = [7, 8, 12, 11, 19, 27, 43, 67, 163] #h=[1,2,3,7,11,19,43,67,163] jj = [3375, -8000, -54000, -32768, -884736, -12288000, -884736000, -147197952000, -262537412640768000] # set a half of bit size random number ii = 0b1010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 for i in 0..1000000 for j in 0..8 for u in 0..6500 uu = (ii * ii) + h[j] * @uint16_prime[u] * @uint16_prime[u] @V = -1 if (uu % 4 == 0) oo = uu / 4 mm = oo + 1 - ii k = 0 cond = 0 if (oo % 2 == 1 && 4 * oo - ii * ii == h[j] * @uint16_prime[u] * @uint16_prime[u]) k = k + 1 while (@uint16_prime[k] != 65521) # cout << k << " "; if (oo % @uint16_prime[k] == 0 || mm % @uint16_prime[k] == 0) cond = 0 break end if (oo % @uint16_prime[k] != 0 && mm % @uint16_prime[k] != 0) cond = 1 end k = k + 1 end if (cond == 1) if (fr(oo) == 1 && fr(mm) == 1) print "\n" # print "t=", ii, "\n"; # print "j=", jj[j], "\n" # print "p=", oo, "\n" # print "m=", mm, "\n" m2 = 2 * oo - mm print "m2=", m2, "\n" k = (jj[j]) * inv(1728 - jj[j], oo) % oo; a = 3 * k % oo; b = 2 * k % oo # print "a=", a, "\n" # print "b=", b, "\n" # k=jj[j]*inv(1728+jj[j],oo)%O; if (k < 0) while (k < 0) k = k + oo end end @CRV_a = 3 * k % oo @CRV_b = 2 * k % oo pgcm_G_y = PC(@CRV_b, oo) @CRV_n = mm @CRV_p = oo if (pgcm_G_y > 0) pgcm_G_x = 0 @G_x = 0 @G_y = pgcm_G_y v = mktable(@G_x, @G_y) if (v != -1) print "a=", @CRV_a, "\n" print "b=", @CRV_b, "\n" print "n=", mm, "\n" print "p=", oo, "\n" ellip(mm) print "v=", @V, "\n" if (@V == 0) bit(mm); print "-bit prime\n"; w() print "ママ!ここにいたのね!\n" end if (@V == -1) p = inv(@CRV_a, @CRV_p) * (-3) % @CRV_p print (@CRV_p - @CRV_a * p) % @CRV_p, "\n" print p, "\n" c = PC(p, @CRV_p) if (c != -1) # print "E' twist!\n" #print "a'=",@CRV_a=(@CRV_a*c*c%@CRV_p)-@CRV_p,"\n" #print "b'=",@CRV_b=(@CRV_b*c**3)%@CRV_p,"\n" #y=PC(@CRV_b,@CRV_p) #if(y != -1) # mktable(0,y) # ellip(oo+1+ii) # if(@V==0) # bit(m2); print "-bit_m2 prime\n"; w(); # print "(・∀・)イイ!!\n"; # exit(); # end # end end end #end end end end end end end end end ii = ii + 1 end end このプログラムは上記の本にあったことを参考に、自分のアレンジで書いたものだ。 オリジナルの方法ではまず素数を決めて、その上でコルネッキアという式を解いて高い類数の曲線を計算する方法を取る。 しかし高次の多項式を解くのは巨大整数なので遅くなる。その代わり、いろんな素数を試して簡単に曲線を生成したいと思ってこの方法にした。だから類数は1の場合に限られる。この方法では、素数をスライドさせて使い捨てにする。素数判定自体は簡単にできるはずなので、総当りで素数を試していけばいつかは素数位数の点を持つ楕円曲線に当たるだろうというナイーブな方法である。こんな方法でもいろいろ楕円曲線が見つかるのだから不思議である。 そして実験的にわかることは、ヒーグナー数が163の楕円曲線がよく見つかり、一見するとその存在が偏っているかのように見えることだ。これはこの方式のバグなのかどうか検討が必要である。 この実装では便宜的に巨大整数を扱うためにrubyで書いてあるが、C++で書いたほうがOpenMPとかを使って並列計算できるし更に速くなる可能性がある。また、ツイストと呼ばれる曲線を取りこぼしているのが残念である。今後修正するかもしれない。 同種写像暗号はまだ出来たばかりのものなので、かつての楕円曲線がそうだったように今後の研究の結果使えないものになる可能性もある。今後どのようなアルゴリズムや攻撃が考案されるかが気になるところである。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

転売のための商品大量購入防止アプリとして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で続きを読む