20200721のRailsに関する記事は17件です。

【Rails】renderメソッドの引数のカッコの有無について

renderメソッドの引数にはカッコはつける?つけない?

結論から言うと、どちらでもOKです。
Rubyにおけるメソッドの呼び出しではカッコを省略することができます。

なので以下の2つはどちらの書き方でも問題ありません。

posts_controller.rb
render("posts/new")
posts_controller.rb
render "posts/new"
※ただし、複雑なコードを書く場合は、区切りの位置をわかりやすくする為にカッコをつけたほうがいいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails c でkey errorが出た時

以前FactoryBotの確認でコンソールをいじっている時にそれは起こった。

irb(main):001:0> FactoryBot.create(:user)
Traceback (most recent call last):
        1: from (irb):1
KeyError (Factory not registered: "user")

FactoryBotの中身を確認してみる。。

FactoryBot.define do
  factory :user do
    name {Faker::Name.last_name}
    email {Faker::Internet.free_email}
    password = Faker::Internet.password(min_length: 8)
    password {password}
    password_confirmation {password}
  end
end

FavtoryBotのエラーについて調べたが()で閉じることでエラーが出ることが多いみたい。でも{}で中身入れてるし問題ない。

調べていくうちにrailsコマンドが正常に作動しない時はspringを止めたら動き始めるとのことが発覚。

springはrailsが持っている昨日で入力したrailsコマンドの起動を早くしてくれるらしいがたまに固まってしまうらしい。

spring stop

これで無事動き始めました。

安心してください、、、またrailsコマンドを使うと自動的にspringは起き上がります!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マークダウン記法でDB設計【残り55日】

使用したマークダウン記法
[#]
見出しとして使用
数を増やすとfont-sizeが小さくなっていく
HTMLでいうh1~h6タグ

[|文字列|]
表の作成として使用
コロンを文字列の左、左右、右につけることで左寄せ、中央揃え、右寄せにできる

[-]
箇条書きとして使用
ハイフンの後に半角スペースを入れること

マークダウン記法でDB設計.png

テーブル設計

users テーブル

Column Type Option
name string null: false
email string null: false
password string null: false

Association

  • has_many :room_users
  • has_many :rooms, through :room_users
  • has_many :messages

カラム、アソシエーションが一目瞭然なのでアプリケーション開発には必須の作業。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]Administrateでメインアプリで使っていたヘルパーメソッドを動作させる方法

困ったこと、やりたいこと

\\\メインアプリのヘルパーメソッドが使えない///

Railsアプリで管理画面を作成するために、Administrateというgemを使用した時のことです。
メインアプリのビューで使うためにhelperファイルに定義したhelperメソッドを、admin以下のビューでも使おうとしたところ、下記のエラーが…

スクリーンショット 2020-07-21 21.00.03.png

image_present?は、メインアプリのビューで使用しているヘルパーメソッド(自分でhelpers/shops_helper.rbに定義したもの)です。
メインアプリではNoMethodErrorにならないのに…どうすればいいの???

環境

  • Ruby 2.5.1
  • Rails 5.0.7.2
  • Administrate 0.14.0

結論

1. config/application.rbファイルに、下記の設定を記述する。
2. アプリケーション(サーバー)を再起動する

以上。簡単でした笑

config/application.rb
module Hoge
  class Application < Rails::Application
    # 他のconfigが書かれてるかも

    # ここから
    config.to_prepare do
      Administrate::ApplicationController.helper Hoge::Application.helpers
    end
    # ここまで
  end
end

Hogeはアプリ名です。通常ならば、1行目のmoduleの後ろにrails newした時のアプリ名が記載されているはずです。

Administrate::ApplicationController.helper Hoge::Application.helpers

Hogeの部分を、ご自身のアプリ名に置き換えてください。

しっかりGitHubのIssuesに挙げられていて、解決済みだった

Best way to include main app helper modules in administrate? #334
スクリーンショット 2020-07-21 20.51.34.png

余談

参考記事に挙げたGitHubのIssuesを見ていると、他の方法もあるよう。

スクリーンショット 2020-07-21 20.52.13.png

こちらの方法は、
1. helpersディレクトリの下にadministrateディレクトリを作成
2. そこにapplication_helper.rbファイルを作成
3. そこにヘルパーメソッドを再度定義する
という方法のようです。

試しにやってみた

helpers/administrate/application_helper.rb
module Administrate::ApplicationHelper
  def image_present?(shop)
    if shop.shop_images.present?
      image_tag "#{shop.shop_images[0].shop_image}", class: 'top-cover-img shop-img'
    else
      image_tag asset_path('no-image.png'), class: 'top-cover-img shop-img'
    end
  end
end

ポイントは、moduleの宣言の際に、クラスの継承?(Administrate::ApplicationHelper)を記述することだそうです。
これがないとRouting Errorになります。
スクリーンショット 2020-07-21 21.10.13.png

実現できるが、DRY原則に反する

一応上記の方法でも実現できましたが、同じヘルパーメソッドを2回定義することになるので、DRY原則に反するなと思いました。

無駄なファイルも作成しなくていいので、冒頭の方法をおすすめします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで、チャット機能を実現する

Railsで、チャット機能を実現する

実現したいこと

  • UserとProducer間で、チャットができるようにしたい。
  • 基本的にはUserからしかチャットを持ちかけることはできない。
  • 「User」としてログインした際に、商品の詳細ページから、「生産者にDMする」ボタンを押すと、チャットページに遷移し、商品を出品している生産者と利用でDMができる。
  • 「Producer」としてログインした際は、「DMリスト一覧」ボタンを押すと、Producerとチャットしたことのあるユーザー一覧が表示される。そこから各ユーザーとのチャット画面に遷移することができる。

前提

2つのUser間でチャットが行われる前提なので、2つのモデル(UserモデルとProducerモデルなど)をdeviseで作成してあることが前提となります。

モデル作成

$ rails g model room user_id:integer producer_id:integer
$ rails g model message room_id:integer is_user:boolean content:text
$ rails db:migrate

アソシエーション設定

user.rb
  has_many :rooms
producer.rb
  has_many :rooms
room.rb
has_many :messages
belongs_to :producer
belongs_to :user

ER図はこのようになるかと思います。
image.png

コントローラー作成

$ rails g controller rooms index show
$ rails g controller messages

ルーティング修正

routes.rb
Rails.application.routes.draw do

  root to: 'home#index'
  devise_for :producers
  devise_for :users
  resources :products do
  resources :users, only: [:show]
  resources :rooms, only: [:index,:show, :create] do
    resources :messages, only: [:create]
  end

end

roomsの中にmessagesをネストすることによって、messageのcreateアクションのURLは/rooms/:room_id/messages(.:format)になります。

roomsコントローラー記述

rooms_controller.rb
class RoomsController < ApplicationController
  def index #producerとしてログインした際の、「DMリスト」画面
      # チャットページへの遷移処理追加
    if producer_signed_in? # producer がサインインしていたら
      @users = User.all #全てのuserを取得
      rooms = current_producer.rooms #現在ログインしているproducerが所持している全ての rooms を取得
      @user_ids = [] #配列を用意
      rooms.each do |r| #「現在ログインしている Producer が所持している全てのrooms」を1つずつ取り出す
        @user_ids << r.user_id #ログインしている Producer のチャット相手、つまり user の user_id を @user_ids に格納
      end  
      #これにより、 user(チャット相手)の user_id があるかどうかview側で判断して、「チャットしたことある相手なのかどうか」を判断している
    end
  end

  def show #トークルーム画面
    @room = Room.find(params[:id]) #ルーム情報の取得( user_id, producer_id が入っているかどうかを調べる)
    @message = Message.new #新規メッセージ作成
    @messages = @room.messages

    if user_signed_in? #もし、ユーザーがサインインしていて
      if @room.user_id == current_user.id #かつ、roomに入っている user_id が、 現在ログインしているユーザーのidだったら
        @producer = @room.producer # @producerに、現在 room に入っている producer の情報を全て格納する
      else
        redirect_to "/"
      end

    elsif producer_signed_in? #もし、producerがログインしていて
      if @room.producer_id == current_producer.id #かつ、 roomに入っている producer_id が、 現在ログインしている producer のidだったら
        @user = User.find_by(id: @room.user_id) # @userに、現在 room に入っている user の情報を全て格納する
      else
        redirect_to "/"
      end
    end
  end

  def create
    if user_signed_in? #もしユーザーがサインインしていたら
      @room = Room.new(room_producer_params)
      @room.user_id = current_user.id # @room の user_id に、 現在ログインしているユーザーのidを代入する。 
    else
      redirect_to "/"
    end

    if  @room.save
      redirect_to action: "show", id: @room.id
    else
      redirect_to "/"
    end
  end

  private

  def room_producer_params
    params.require(:room).permit(:producer_id)
  end


end

createアクションについて説明していきます。
これは、商品詳細ページから、userとproducerのチャットルームが作られる時の挙動を表しています。
チャットルームが作られる際の条件としては、 user_id と producer_id が必要です。
Room.new(room_producer_params)で、html側のhidden_fieldでproducer_idを送信しています。
@room.user_id には、現在ログインしているユーザーのidを代入しています。
これで2つ揃ったので、チャットルームが作成されます。

if @room.save
redirect_to action: "show", id: @room.id
else

で、チャットルームが作成されたら、チャット画面に遷移する。どのチャット画面に遷移すればいいのかわからないので、@room.idを引数に渡しています。

messagesコントローラー記述

messages_controller
class MessagesController < ApplicationController
  def create
    @room = Room.find(params[:room_id]) # Roomモデルから、 該当のroomを取ってくる
    @message = Message.new(message_params) #新規メッセージ作成
    #メッセージがuserによるものだったらis_user=true, producerによるものだったらis_user=false
    if user_signed_in? #もしユーザーがサインインしていたら
      @message.is_user = true # is_userをtrueにして、 userの発言としている
    elsif producer_signed_in? #もし producer がサインインしていたら
      @message.is_user = false # is_userをtrueにして、 producerの発言としている
    end
    @message.room_id = @room.id #messageのroom_idに、現在のルームのidを代入する。 これで、room_id と content が揃うので、message を save 出来る
    if @message.save # メッセージを save できたら
      redirect_to room_url(@room) #現在のroomに遷移する
    else
      redirect_to room_url(@room)
    end
  end

  private
  def message_params
    params.require(:message).permit(:content)
  end
end

html記述(商品詳細ページ)

show.html.erb

<h2>商品詳細</h2>
<% if producer_signed_in? && @product.producer == current_producer %>
  <%= link_to "編集", edit_product_path(@product) %>
<% end %>
<p>商品名:<%= @product.name %></p>
<p>内容:<%= @product.content %></p>
<p>値段:<%= @product.price %></p>
<p>内容:<%= @product.description %></p>

<% if @product.image.attached? %>
  <p><%= image_tag @product.image %></p>
<% end %>

<p><%= @product.producer.prefecture.name %></p>
<p><%= @product.producer.name %></p>

<% if @product.producer.image.attached? %>
  <p><%= image_tag @product.producer.image %></p>
<% end %>


<% if user_signed_in? %>

  <% if @producer_ids.include?(@producer.id)  %> <%#ユーザーとしてログインした時に、相手のidが存在するかどうかをチェックしている  %>
    <br>
    <%= link_to "チャットへ", room_path(current_user.rooms.find_by(producer_id: @producer.id)) %>
  <% else %>
    <%= form_for Room.new do |f| %>
      <%= f.hidden_field :producer_id, :value => @producer.id %>
      <%= f.submit %>
    <% end %>
  <% end %>
<% end %>

html記述(チャットルームページ)

show.html.erb
<h1>チャットルーム</h1>

<% if user_signed_in? %>
  <h3><%= @producer.name %>とのチャットルーム</h3>
  <div class="chat-field">
    <% @messages.each do |m| %>
      <!-- メッセージがUserによるものだったら -->
      <% if m.is_user %>
        <!-- メッセージを右に寄せる -->
        <div class="right-message">
          <%= m.content %>
        </div>
      <!-- メッセージがproducerによるものだったら -->
      <% else %>
        <!-- メッセージを左に寄せる -->
        <div class="left-message">
          <%= m.content %>
        </div>
      <% end %>
    <% end %>
  </div>

<% elsif producer_signed_in?%>
  <h3><%= @user.name %>とのチャットルーム</h3>
  <div class="chat-field">
    <% @messages.each do |m| %>
      <!-- メッセージがUserによるものだったら -->
      <% if m.is_user %>
        <!-- メッセージを左に寄せる -->
        <div class="left-message">
          <%= m.content %>
        </div>
      <!-- メッセージがproducerによるものだったら -->
      <% else %>
        <!-- メッセージを右に寄せる -->
        <div class="right-message">
          <%= m.content %>
        </div>
      <% end %>
    <% end %>
  </div>
<% end %>

<%= form_for [@room, @message] do |f| %>
  <%= f.text_field :content %>
  <%= f.submit "メッセージを送る"%>
<% end %>

is_userがtrueかfalseかで、CSSで表示を分けています。

html記述(DM一覧ページ)※Producerとしてログインした場合にしか表示されない

index.html.erb
<h1>rooms#index</h1>

<% @users.each do |user| %>
  <% if user.rooms.present? %>
    <%= user.name %>
      <% if @user_ids.include?(user.id) %> 
        <br>
        <%= link_to "チャットへ", room_path(current_producer.rooms.find_by(user_id: user.id)) %><br>
      <% end %>
  <% end %>
<% end %>

<% if user.rooms.present? %>
に関して、コントローラー側で@users=User.allで全てのユーザーを取ってきています。
ユーザーが所持しているroomsの値は存在するか? = Producerとチャットしたことがあるユーザーだけをフィルタリングしています。

参考資料

Railsでモデルを複数使用しチャット(DM)機能を作成する - Qiita
の記事を参考にさせていただきました。ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】【CRUD】Vue.js(Nuxt.js)とRailsでユーザー新規登録・ログイン・退会・ログアウト・編集を実装してみる

はじめに

Vue.js(Nuxt.js)とRailsで新規開発を行っており、JWTを用いてユーザー新規登録・ログイン・ログアウト等の認証周りを担当したのでここに記しておきます。備忘録。
今回はauthmoduleとdevise_token_authを使用して実装しました。
別々で解説している記事はあったのですが、全体の流れがわかる記事が少なかったので、執筆しました:frowning2::frowning2:
この記事が見知らぬ誰かの糧となれば良いなあと思っております〜〜:muscle_tone2:
authmodule公式
devise_token_auth公式

環境

  • MacOS
  • yarn 1.21.1
  • node 12.0.0
  • vue 2.6.1
  • rails 5.2.4
  • ruby 2.6.3

新規登録の流れ

Nuxt側: APIにEメールパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。

プラグインの導入

Axiosを導入

Nuxt側にaxiosを追加します。
axios公式

ターミナル
$ yarn add @nuxtjs/axios

Auth Moduleを導入

Nuxt側にAuth Moduleも追加します。

ターミナル
$ yarn add @nuxtjs/auth

Vuex用にindex.jsを作成

Auth moduleはVuexを使用して、ユーザの認証情報を管理します。
そこで、Vuex用のindex.jsファイルを用意しておく必要があります。
nuxtアプリのルートディレクトリにstoreというファイルが作成されるので、そこにindex.jsという名前のファイルを作成しておきます。index.jsの中はなにも記載しなくて大丈夫です。
スクリーンショット 2020-07-15 17.15.55.png

Nuxt

nuxt.config.jsにプラグインを記載

追加したaxiosとauthをnuxtアプリに読み込ませます。

nuxt.config.js
modules: [
  '@nuxtjs/axios',
  '@nuxtjs/auth'
]

nuxt.config.jsにauthオプションを記載

authで用意されているオプションを記載していきます。

nuxt.config.js
auth: {
  redirect: {
      login: '/users/login',
      logout: '/',
      callback: false,
      home: '/users/profile',
  },
  strategies: {
    local: {
      endpoints: {
        login: { url: '/api/v1/auth/login', method: 'post', propertyName: 'token' },
        logout: { url: '/api/v1/auth/logout', method: 'post' },
        user: false,
      },
    }
  }
}

redirect

ユーザーのアクションに応じたリダイレクト先のURL設定。

  • login:未ログイン時にリダイレクトされる先のURL
  • logout:ログアウトした後にリダイレクトされる先のURL
  • callback:コールバック用のURL。Oauth認証(SNS認証)等に使われる。
  • home:ログイン後にリダイレクトされる先のURL

strategies

Auth Moduleの認証ロジックの設定です。JWTとCookieを使うlocalと、OAuthを使うsocialの2種類の設定ができます。今回はOauth認証は使わないので、socialは記載していません。

  • endpoint:どのメソッドが呼ばれた際に、APIのどのエンドポイントに飛ばすかを設定します。例えば、loginメソッドが呼び出されたら、APIにpost: /api/auth/loginにHTTPリクエストを送信します。

ログイン

ログイン画面の実装です。
vuetifyというUIフレームワークを使用しています。template部分は適宜変えてください。

users/login
<template>
  <v-container>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">
          ログイン
        </h1>
      </v-card-title>
      <v-card-text>
        <v-form ref="form" lazy-validation>
          <v-text-field
            v-model="email"
            prepend-icon="mdi-email"
            label="メールアドレス"
          />
          <v-text-field
            v-model="password"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード"
          />
          <v-card-actions>
            <v-btn
              color="light-green darken-1"
              class="white--text"
              @click="loginWithAuthModule"
            >
              ログイン
            </v-btn>
          </v-card-actions>
        </v-form>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script>
export default {
  name: 'App',
  auth: false,
  data() {
    return {
      password: '',
      email: '',
    }
  },
  methods: {
    // loginメソッドの呼び出し
    async loginWithAuthModule() {
      await this.$auth
        .loginWith('local', {
         // emailとpasswordの情報を送信
          data: {
            email: this.email,
            password: this.password,
          },
        })
        .then(
          (response) => {
       // レスポンスで返ってきた、認証に必要な情報をlocalStorageに保存
            localStorage.setItem('access-token', response.headers['access-token'])
            localStorage.setItem('client', response.headers.client)
            localStorage.setItem('uid', response.headers.uid)
            localStorage.setItem('token-type', response.headers['token-type'])
            return response
          },
          (error) => {
            return error
          }
        )
    },
  },
}
</script>

スクリーンショット 2020-07-15 18.33.17(2).png
見た目はこんな感じですね。vuetifyすごい、、、

methodの部分をざっくり説明すると、

1.入力されたemailとpasswordの情報をloginメソッドを用いて、APIに送信する。
2.RailsAPI側からのレスポンスから認証機能に必要な情報(access-token・client・uid・token-type)をlocalStorageに保存する

ということをやっています。他にもやり方はありますし、仕様によっても異なるので、あくまで参考程度に、、、
ただ、APIに入力された情報を送信して、認証機能に必要な情報を保持するというのは基本的に同じです。
この仕組みをある程度理解しておくと、応用できると思います。

新規登録

新規登録画面の実装です。

users/signup
<template>
  <v-container>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">
          新規登録
        </h1>
      </v-card-title>
      <v-card-text>
        <v-form ref="form" lazy-validation>
          <v-text-field
            v-model="user.email"
            prepend-icon="mdi-email"
            label="メールアドレス"
          />
          <v-text-field
            v-model="user.password"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード"
          />
          <v-text-field
            v-model="user.password_confirmation"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード確認"
          />
          <v-card-actions>
            <v-btn
              color="light-green darken-1"
              class="white--text"
              @click="registerUser"
            >
              新規登録
            </v-btn>
          </v-card-actions>
        </v-form>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script>
export default {
  name: 'App',
  auth: false,
  data() {
    return {
      user: {
        password: '',
        email: '',
        password_confirmation: '',
      },
    }
  },
  methods: {
    registerUser() {
      this.$axios.post('/api/v1/auth', this.user).then((response) => {
        window.location.href = '/users/comfirmation'
      })
    },
  },
}
</script>

auth moduleには新規登録のヘルパーメソッドはないので、axiosを使ってメソッドを自作します。
といっても、registerUserが発火されたら新規登録のエンドポイントにメールアドレスやパスワードの必要情報を送信するだけです。
スクリーンショット 2020-07-20 18.11.30.png

見た目は各自適当に弄ってください!!

ログアウト・退会

users/account
<template>
  <v-app>
    <v-container>
      <v-row>
        <v-spacer></v-spacer>
        <v-col cols="12" lg="4">
          <v-row>
            <v-col
              cols="12"
              lg="7"
              class="grey--text text--darken-3 font-weight-bold pa-2 text-h6"
            >
              <p>
                アカウント設定
              </p>
            </v-col>
          </v-row>
          <v-row class="my-5">
            <v-col cols="12" lg="7" class="pa-2">
              <a
                href="/"
                class="grey--text text--darken-3 mb-1"
                @click="$auth.logout()"
              >
                ログアウト
              </a>
            </v-col>
            <v-col cols="12" lg="5" class="pa-2 text-right">
              <font-awesome-icon icon="angle-right" />
            </v-col>
          </v-row>
          <v-divider></v-divider>
          <v-row class="my-5">
            <v-col cols="12" lg="7" class="pa-2">
              <a
                href="#"
                class="red--text text--darken-3 mb-1"
                @click="deleteUser"
              >
                退会
              </a>
            </v-col>
            <v-col cols="12" lg="5" class="pa-2 text-right">
              <font-awesome-icon icon="angle-right" />
            </v-col>
          </v-row>
        </v-col>
        <v-spacer></v-spacer>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data: () => ({}),
  methods: {
    deleteUser() {
      this.$axios
        .delete('api/v1/auth', {
          headers: {
            'access-token': localStorage.getItem('access-token'),
            uid: localStorage.getItem('uid'),
            client: localStorage.getItem('client'),
          },
        })
        .then((response) => {
          this.$auth.logout()
          window.location.href = '/'
        })
    },
  },
}
</script>

ログアウトはauth moduleにヘルパーメソッドがあるので利用しましょう。
クリックと同時に$auth.logout()を発火すれば、完了です。
header内に保存されていた認証情報がすべて削除され、未ログイン状態になります。

退会は新規登録と同様にメソッドを作ります。
axiosでエンドポイントに認証情報を付与して送信します。httpメソッドはdeleteですね。
その後、ログアウトメソッドを実行しとけばエラーにならず、安心です。
簡単ですね。

編集

users/edit
<template>
  <v-app>
    <v-container>
      <v-card width="400px" class="mx-auto mt-5">
        <v-card-title>
          <h1 class="display-1">
            メールアドレス変更
          </h1>
        </v-card-title>
        <v-card-text>
          <v-form ref="form" lazy-validation>
            <v-text-field
              v-model="user.email"
              prepend-icon="mdi-email"
              label="新しいメールアドレス"
            />
            <v-text-field
              v-model="user.password"
              prepend-icon="mdi-lock"
              append-icon="mdi-eye-off"
              label="パスワード"
            />
            <v-card-actions>
              <v-btn
                color="light-green darken-1"
                class="white--text"
                @click="editEmail"
              >
                保存する
              </v-btn>
            </v-card-actions>
          </v-form>
        </v-card-text>
      </v-card>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      user: {
        password: '',
        email: '',
      },
    }
  },
  methods: {
    editEmail() {
      this.$axios
        .put('api/v1/auth', this.user, {
          headers: {
            'access-token': localStorage.getItem('access-token'),
            uid: localStorage.getItem('uid'),
            client: localStorage.getItem('client'),
          },
        })
        .then((response) => {
          localStorage.setItem('access-token', response.headers['access-token'])
          localStorage.setItem('client', response.headers.client)
          localStorage.setItem('uid', response.headers.uid)
          localStorage.setItem('token-type', response.headers['token-type'])
          window.location.href = '/'
        })
    },
  },
}
</script>

メールアドレスの編集ページです。見た目はほとんど新規登録・ログイン画面と同じです。
編集は認証情報を渡して、受け取らなければなりません。
リクエストで認証情報を付与して送信する。成功したら受け取った認証情報をlocalStrageに保存する。
理由はアカウント編集すると認証情報が変わるからです。
これで編集もできました。

auth module補足

一連の流れを見ていただいた方ならおわかりかと思いますが、auth moduleはそこまで機能がもりもりではありません。
すべてのメソッドを作ってくれているわけではなく、ある程度は自力で頑張る必要があります。
認証機能の補助輪くらいに考えておいてください!!
ただ、リダイレクト先の指定やloginメソッドなど優秀な機能もありますので、Nuxtで認証機能を実装する方は是非使ってみてください。
他に認証機能で良いプラグインがあれば教えて下さい:joy::joy:

devise_token_auth公式

Rails

フロント側(Nuxt側)ばかりやってきましたが、APIが飛ばないとお話になりませんので、バックエンド側(Rails側)もやっていきましょう!!

devise_token_authを導入

Gemfileにdevise_token_authとrack-corsを記載し、bundle installをします。

Gemfile
# ログイン機能 
gem 'devise' 
gem 'devise_token_auth'

# CORS設定
gem 'rack-cors'
ターミナル
$ rails g devise:install
$ rails g devise_token_auth:install User auth

これで色々ファイルが生成されると思います。
※すでにUserモデルがある方は設定方法が少し変わるので、対処法は公式を読んでください。

DB作成

db/migrate/~_devise_token_auth_create_users.rb
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.2]
  def change

    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, :default => false

      ## Rememberable
      t.datetime :remember_created_at

      # ここを追記 --------------------------------------------
      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip
      # -----------------------------------------------------

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      ## User Info
      t.string :name
      t.string :nickname
      t.string :image
      t.string :email

      ## Tokens
      t.text :tokens

      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,       unique: true
  end
end

migrateしてDBを作成します。

devise_token_authの設定

config/initializers/devise_token_auth.rb
DeviseTokenAuth.setup do |config|
  # リクエストごとにトークンを更新するか
  config.change_headers_on_each_request = false

  # トークンの有効期間
  config.token_lifespan = 2.weeks

  # headersの名前対応
  config.headers_names = {:'access-token' => 'access-token',
                          :'client' => 'client',
                          :'uid' => 'uid',
                          :'token-type' => 'token-type' }
end

ここで諸々の設定をします。
トークンの有効期限やheadersの送信名もここで設定します。

controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  skip_before_action :verify_authenticity_token, if: :devise_controller?
end

APIではCSRFチェックをしないようにapplication_controller.rbをすこしいじったら、
準備完了です。

ルーティング設定

routes.rb
Rails.application.routes.draw do
  devise_for :users

  namespace :api do
    scope :v1 do
      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end
end

公式通りだとこんな感じですね。

リクエストテスト

では実際にリクエストを投げてみましょう。
今回はログイン機能で試してみます。
※email:example@example.com,password:passwordというユーザーが登録されている想定です。
スクリーンショット 2020-07-21 10.04.08(2).png

ログイン機能のエンドポイントである/api/v1/auth/sign_inにpostでリクエストを送信します。
Content-Typeはapplication/jsonを選択しておきましょう。
BODYにemailとpasswordの情報をjson形式で付与します。

これでリクエストを投げます。

うまく行けばステータスコード200でレスポンスが返ってきます。
スクリーンショット 2020-07-21 10.31.12(2).png

こんな感じですね。

新規登録やログアウトも同様です。
エンドポイント、HTTPメッソド、付与情報を変更して、リクエストを送れば、それに応じたレスポンスが返ってくるはずです。

devise_token_auth補足

devise_token_authではdeviseと同じようにbefore_action :authenticate_user!current_userも使えます。
フロント側で保持しておくべき情報も基本的にはレスポンスで返してくれるので、それを取り出してlocalStrage等で保存しておきましょう。

詳しくは公式を読んでみてください。
devise_token_auth公式

完成!!!

これで完成です!!!長かったですね、、、
フロント側(Nuxt側)もバックエンド側(Rails側)もバッチリだと思います。
もう一度、新規登録を例にとって流れを振り返ります。

Nuxt側: APIにEメールパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。

これができているはずです。
ログインや退会等のその他の機能も流れは基本的に変わりません!!

最後に

auth moduleとdevise_token_auth別々の解説記事はあったのですが、まとまった記事(流れがわかるような記事)がなかったため、今回執筆しました。
長かった、、、かなり、、、
もしわからない点があれば気軽にコメントしてくださいね〜:ok_hand_tone2::ok_hand_tone2:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで日時を日本語表記に変更

はじめに

テーブルのカラム created_at を使えば、データを作成した日時を取り出せます。
ところが、そのままの表記では使いづらいです。

今回は、 2020/7/21 9:05:50 の様な表記で取り出せるようにしたいと思います。

作成日時を表示する

まず posts テーブルのデータ @postcreated_at をそのまま取り出してみましょう。

posts/show.html.erb
<p>イベント: <%= @post.event %></p>
<p>場所: <%= @post.place %></p>
<p>作成日時: <%= @post.created_at %></p>
place event created_at
東京 花火大会 2020-07-21 00:05:50 UTC

目標としている 2020/7/21 9:05:50 の表記と大いに違うことが分かると思います。

具体的には以下の3点が異なります。

  1. UTC (世界標準時) になっている
  2. / のところが - になっている
  3. の表記が 07 になっている

順番に修正していきたいと思います。

1.世界標準時から日本時間に変更する

プログラミングは世界中で使われている為、そのままだと 世界標準時 で出力されてしまいます。
その為、日本時間 に変更しましょう。

config/application.rb
class Application < Rails::Application
  # ***** 次の1行を追加 *****
  config.time_zone = "Asia/Tokyo"
end

この1行を入れることで以下のように、世界標準時日本時間 に変更することができました。

place event created_at
東京 花火大会 2020-07-21 09:05:50 +0900

+0900 がついていますが、これは、日本時間と世界標準時との時差が+9時間であることを表しています。

2.フォーマットを変更する

日本時間にはなりましたが、フォーマットが 2020-07-21 09:05:50 +0900 では分かりにくいです。変更していきましょう。

まず、多言語に対応する為の gem をインストールし、日本語に設定します。

Gemfile
# Rails6 の場合
gem 'rails-i18n', '~> 6.0'
# Rails5 の場合
gem 'rails-i18n', '~> 5.1'
ターミナル
bundle install
config/application.rb
class Application < Rails::Application
  # 略
  config.time_zone = "Asia/Tokyo"
  # ***** 次の1行を追加 *****
  config.i18n.default_locale = :ja
end

次に、 rails-i18n に用意されている lメソッドを使用します。

posts/show.html.erb
<p>イベント: <%= @post.event %></p>
<p>場所: <%= @post.place %></p>
<!-- ***** l を追加 ***** -->
<p>作成日時: <%= l @post.created_at %></p>

さらに、config/locales/ja.ymlというファイルを作成し、フォーマットを設定しましょう。

config/locales/ja.yml
ja:
  time:
    formats:
      default: "%Y/%m/%d %H:%M:%S"

これにより、表記が以下のように変わり、+0900も消えます。

place event created_at
東京 花火大会 2020/07/21 09:05:50

フォーマットは自由に変更することができ、例えば、 が不要なら :%S を省くことで対応出来ます。

config/locales/ja.yml
      default: "%Y/%m/%d %H:%M"
place event created_at
東京 花火大会 2020/07/21 09:05
記号 意味
%Y 年(YEAR)
%m 月(MONTH)
%d 日(DAY)
%H 時(HOUR)
%M 分(MINUTE)
%S 秒(SECOND)

[参考ページ] Ruby 2.7.0 リファレンスマニュアル(strftime)

3.ゼロ(0)埋めをなくす

例えば、 7月07 と表記されるのは不自然なので 0 を削りましょう。

config/locales/ja.yml
ja:
  time:
    formats:
      # ***** %の後ろに-を追加 *****
      default: "%Y/%-m/%-d %-H:%M:%S"
place event created_at
東京 花火大会 2020/7/21 9:05:50

-を入れることで以上の様に、0埋め をなくすことが出来ました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでRailsのセットアップ

① develop > docker > app名(test) を作成

② test内に、'dockerfile'、'docker-compose.yaml'を作成

③ 'dockerfile'編集

dockerfile
FROM ruby:2.6.6-stretch

Docker は Dockerfile から命令を読み込んで、自動的にイメージをビルドします。 Dockerfile はテキストファイルであり、イメージを作り上げるために実行するコマンドラインコマンドを、すべてこのファイルに含めることができます。 docker build を実行すると、順次コマンドライン命令を自動化した処理が行われて、ビルド結果となるイメージが得られます。

④ 'docker-compose.yaml'編集

docker-compose.yaml
version: '3'
services:
  app:
    build: .
    volumes:
      - ".:/app"
    ports:
      - "3000:3000"
    tty: true

⑤ ターミナル

docker-compose up
# コンテナの起動・構築

6 コンテナを作成して起動からログインしてbashで操作

docker exec -it SAMPLE_APP_1 /bin/bash

Dockerでよく使うコマンドまとめ

https://designsupply-web.com/media/knowledgeside/3325/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails API + SPA] ログイン方法とCSRF対策例

記事の趣旨

モダンなWebアプリ開発の学習として、Vue.jsによるフロント、Rails APIによるバックエンドで実装するSPAを作りました。

その中で、SPAの様々なログイン実装方法や、それに必要な CSRF 対策を学びました。

最終的に session + cookie を利用するログインを実装した過程を記録したいと思います。

ログイン実装方法の検討

SPAのログイン実装方法

下記の記事等を読んで、主に JWT を使う方法と、 session を使う方法があることが分かりました。

こちらの記事では、様々な方法の良し悪しが網羅的にまとめられていました。

こちらの記事では、JWTとCookieが端的に比較されていました。

こちらの記事では、 JWTLocal Storage に保存することの危険性が説かれています。

重要なデータを保存する必要があるなら、常にサーバーサイドセッションを使うべきです。

Cookie + session認証

いずれの記事でも、 JWT を単純に Local Storage に保存する方法は避けるべきだと言われています。
そこで、下記の記事の実装例をベースに、 sessionCookie に保存する方法を採用することにしました。

SPA + Rails API 構成におけるcookie + session認証

この方法は、普通のRailsアプリと同じように、 sessioncookies を使うことができるシンプルさが良いと思いました。

この場合、 CSRF 対策を自分で実装する必要があり、それについては後半に扱います。

ログイン機能実装

環境

ruby 2.7.1 rails 6.0.3

Rails APIモードでの立ち上げ

bundle exec rails new アプリ名 --api -TC -d mysql

--api オプションが重要です。

その他は、 -T テストなし(RSpecを使う場合等)、 -C ActionCableなし、 -d mysql DBを指定、というオプションです。

バックエンド側のログイン実装

バックエンド側のログイン機構は、Railsチュートリアルにあるような基本的な方法を使いました。

auth_controller.rb
# フロントから { email: 'メールアドレス', password: 'パスワード' }のようなparamsが送られるものとする

def create
  user = User.find_by(email: params[:email])
  if user&.authenticate(params[:password])
    session[:user_id] = user.id
    payload = { message: 'ログインしました。', name: user.name }
  else
    payload = { errors: ['メールアドレスまたはパスワードが正しくありません。'] }
  end
  render json: payload
end

認証に成功したら、 sessionuser_id を保存しています。(フロントのページに表示するためにユーザー名も返しています。)

ただし、この session[:user_id] = というメソッドをAPIモードのRailsで使うには、下記の設定が必要です。

config/application.rb に下記の3行を追記

参考: Rails の API モードでセッションを有効にする

application.rb
module Myapp
  class Application < Rails::Application
    
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore
    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
  end
end

また、「ログイン状態を保持する」がチェックされた際に、以下のように署名付きCookieに保存するようにしました。

cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token

cookies を使う時には、下記のように ActionController::Cookies をコントローラーにincludeする必要がありました。

参考: rails-apiでcookieを使う

application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::Cookies # 追加
  
end

これで、普通のRailsアプリと同じように、安全なCookieに情報が保存され、リクエストで自動的にCookie情報が送られるようになります。

ログインが必要なアクションでは、下記のような before_action で、 @current_user をセットするようにしました。

application_controller.rb
class ApplicationController < ActionController::API
  before_action :require_login

  private

  def require_login
    @current_user = User.find_by(id: session[:user_id])
    return if @current_user

    render json: { error: 'unauthorized' }, status: :unauthorized
  end
end

CORSの設定

バックエンドとフロントエンドのオリジンが異なる場合は、axiosなどによるフロントからのリクエストを受け取るために、 CORS の設定が必要です。

例えば開発環境で、バックエンドを localhost:3000 、フロントを localhost:8080 で動かしている場合などです。

バックエンド側

gem 'rack-cors' を導入して、 config/initializers/cors.rbに以下のように記述します。

cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end

最後の credentials: true という部分は、Cookieを使えるようにするために必要です。

フロントエンド側

Cookieを使えるようにするため、axiosなどでリクエストする際に、 withCredentials: true というオプションを含めます。

axios({
  method: 'HTTPメソッド',
  url: '送信先URL',
  withCredentials: true, // ここが必要
  headers: { ヘッダー },
  data: { 送信データ },
})

ログイン認証に必要な設定等は以上ですが、このままでは CSRF の攻撃が筒抜けになってしまうので、以降で対策します。

CSRF対策

CSRF(クロスサイトリクエストフォージェリ)

CSRFについては、こちらの記事内の説明が特に分かりやすかったです。

Cookie の性質を利用した攻撃と Same Site Cookie の効果

  • ユーザーが、あるサイト(ターゲット)にログインしたままにしている
  • 別の罠サイト内に、そのターゲットサイトへのリクエスト(何かを変更するなど)を偽装して送る仕掛けがある
  • ユーザーが罠サイトを訪れ、偽装リクエストが送られる
  • ユーザーはターゲットサイトにログインしているため、そのサイトは、偽装リクエストを正常なリクエストとして受け取ってしまう

CSRFとCookie

CSRFは、ターゲットサイトに紐づくCookieにログイン状態が保存されていて、 リクエストがどこから送られたかに関係なく、Cookieが自動的に送信されてしまう ことが利用されています。

なぜ、自サイト内からのリクエストでのみ、Cookieが送信されるようになっていないのでしょうか?

もしそうだった場合、別のサイトからのリンクでターゲットサイトに訪れた時、ログインしていても、Cookieが送られないため、ログインしていないことになってしまいます。そしてリロードすると、自サイト内からのリクエストとしてCookieが送られ、ログインした状態になるという不自然な挙動になります。

RailsのCSRF対策

サーバーサイドレンダリングをするRailsアプリでは、デフォルトでフォームにトークンが仕込まれ、送られてきたリクエストのトークンをチェックすることでCSRF対策をしています。

参考: Rails セキュリティガイド

APIモードでは、フォームにトークンを仕込むことはできないので、別の方法で対策する必要があります。

実験

実際に、上で紹介したログイン機構を持つタスク管理アプリをサーバーに配置し、CSRFの実験をしてみました。

  • 下記のような偽装フォームを持つhtmlを用意し、ローカルサーバーを立ち上げます。
偽装フォーム
<form action="アプリURL" method="post">
  <input type="hidden" name="task[content]" value="偽装タスク">
  <input type="submit" value="送る">
</form>
  • 実際のアプリにログインします。
  • 同じブラウザで、偽装フォームのページを開き、「送る」を押します。
  • アプリは params[:task][:content] を受け取って、「偽装タスク」という内容のタスクを作成してしまいました!

axiosなどを使ってJSでリクエストを送る場合は、CORS設定で許可したオリジン以外は拒否されるのですが、この例のように、formからリクエストを送ると、簡単にCSRFができてしまうことが分かりました。

対策方法

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

こちらの記事で、主に3つの対策方法が紹介されていて、大変参考になりました。

今回はその中から、 固有の HTTP ヘッダを検証する方法 を用いて対策することにしました。

固有の HTTP ヘッダでCSRF対策

リクエストに X-Requested-With: XMLHttpRequest というヘッダを持たせて、バックエンド側では、そのヘッダを含まないリクエストを拒否するという方法です。

この X-Requested-With: XMLHttpRequest は一般的に使われているというだけで、ヘッダ自体には意味がなく、 固有のHTTPヘッダ をつけることに意味があります。それは、

HTML フォーム送信に関しては,一切の余分な HTTP ヘッダの付与が許可されていない。

という制約があるため、上で実験したようにフォームを偽装しても、 X-Requested-With ヘッダをリクエストに含めることができないからです。

したがって、このヘッダを持たない偽装リクエストはバックエンドで弾かれるようになります。

バックエンド側の実装

application_controller に、ヘッダをチェックする before_action を追加します。 X-Requested-With ヘッダの有無をチェックする request.xhr? というメソッドを利用します。

application_controller.rb
class ApplicationController < ActionController::API
  before_action :check_xhr_header

  private

  def check_xhr_header
    return if request.xhr?

    render json: { error: 'forbidden' }, status: :forbidden
  end
end

フロントエンド側の実装

axiosなどでリクエストする際に、 'X-Requested-With': 'XMLHttpRequest' のヘッダを含めます。

axios({
  method: 'HTTPメソッド',
  url: '送信先URL',
  withCredentials: true,
  headers: { 'X-Requested-With': 'XMLHttpRequest' }, // 追加
  data: { 送信データ },
})

以上の実装で、上で行ったCSRF実験で、リクエストを送っても forbidden が返ってくるようになりました。

まとめ

今回実装したログイン方法は、普通のRailsアプリと同じようにsessionを使えるので、初めてのWeb API開発としては、とっつきやすい方法だと思いました。

CSRFの対策は、必要最低限のレベルだと思います。実際のアプリ開発で、もっとセキュリティを高めるために、Cookieの SameSite 属性を組み合わせるなどの方法を学びたいです。

参考記事

SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜

JWT・Cookieそれぞれの認証方式のメリデメ比較

HTML5のLocal Storageを使ってはいけない

SPA + Rails API 構成におけるcookie + session認証

Rails の API モードでセッションを有効にする

rails-apiでcookieを使う

Cookie の性質を利用した攻撃と Same Site Cookie の効果

Rails セキュリティガイド

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

このWeb APIってCSRF対策出来てますか?って質問にこたえよう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails API + SPA】ログイン方法とCSRF対策例

記事の趣旨

モダンなWebアプリ開発の学習として、Vue.jsによるフロント、Rails APIによるバックエンドで実装するSPAを作りました。

その中で、SPAの様々なログイン実装方法や、それに必要な CSRF 対策を学びました。

最終的に session + cookie を利用するログインを実装した過程を記録したいと思います。

ログイン実装方法の検討

SPAのログイン実装方法

下記の記事等を読んで、主に JWT を使う方法と、 session を使う方法があることが分かりました。

こちらの記事では、様々な方法の良し悪しが網羅的にまとめられていました。

こちらの記事では、JWTとCookieが端的に比較されていました。

こちらの記事では、 JWTLocal Storage に保存することの危険性が説かれています。

重要なデータを保存する必要があるなら、常にサーバーサイドセッションを使うべきです。

Cookie + session認証

いずれの記事でも、 JWT を単純に Local Storage に保存する方法は避けるべきだと言われています。
そこで、下記の記事の実装例を参考にさせていただき、 sessionCookie に保存する方法を採用することにしました。

SPA + Rails API 構成におけるcookie + session認証

この方法は、普通のRailsアプリと同じように、 sessioncookies を使うことができるシンプルさが良いと思いました。

この場合、 CSRF 対策を自分で実装する必要があり、それについては後半に扱います。

ログイン機能実装

環境

ruby 2.7.1 rails 6.0.3

Rails APIモードでの立ち上げ

bundle exec rails new アプリ名 --api -TC -d mysql

不要なファイルを省くためのオプションを色々付けていますが、 --api が重要です。

その他は、 -T テストなし(RSpecを使う場合等)、 -C ActionCableなし、 -d mysql DBを指定、というオプションです。

バックエンド側のログイン実装

バックエンド側のログイン機構は、Railsチュートリアルにあるような基本的な方法を使いました。

auth_controller.rb
# フロントから { email: 'メールアドレス', password: 'パスワード' }のようなparamsが送られるものとする

def create
  user = User.find_by(email: params[:email])
  if user&.authenticate(params[:password])
    session[:user_id] = user.id
    payload = { message: 'ログインしました。', name: user.name }
  else
    payload = { errors: ['メールアドレスまたはパスワードが正しくありません。'] }
  end
  render json: payload
end

認証に成功したら、 sessionuser_id を保存しています。(フロントのページに表示するためにユーザー名も返しています。)

ただし、この session[:user_id] = というメソッドは、APIモードのRailsでは無効になっているので、下記の設定が必要です。

config/application.rb に下記の3行を追記

参考: Rails の API モードでセッションを有効にする

application.rb
module Myapp
  class Application < Rails::Application
    
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore
    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
  end
end

また、「ログイン状態を保持する」がチェックされた際に、以下のように署名付きCookieに保存するようにしました。

cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token

cookies を使う時には、下記のように ActionController::Cookies をコントローラーにincludeする必要がありました。

参考: rails-apiでcookieを使う

application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::Cookies # 追加
  
end

これで、普通のRailsアプリと同じように、安全なCookieに情報が保存され、リクエストで自動的にCookie情報が送られるようになります。

ログインが必要なアクションでは、下記のような before_action で、 @current_user をセットするようにしました。

application_controller.rb
class ApplicationController < ActionController::API
  before_action :require_login

  private

  def require_login
    @current_user = User.find_by(id: session[:user_id])
    return if @current_user

    render json: { error: 'unauthorized' }, status: :unauthorized
  end
end

CORSの設定

バックエンドとフロントエンドのオリジンが異なる場合は、axiosなどによるフロントからのリクエストを受け取るために、 CORS の設定が必要です。

例えば開発環境で、バックエンドを localhost:3000 、フロントを localhost:8080 で動かしている場合などです。

バックエンド側

gem 'rack-cors' を導入して、 config/initializers/cors.rbに以下のように記述します。

cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end

最後の credentials: true という部分は、Cookieを使えるようにするために必要です。

フロントエンド側

Cookieを使えるようにするため、axiosなどでリクエストする際に、 withCredentials: true というオプションを含めます。

axios({
  method: 'HTTPメソッド',
  url: '送信先URL',
  withCredentials: true, // ここが必要
  headers: { ヘッダー },
  data: { 送信データ },
})

ログイン認証に必要な設定等は以上ですが、このままでは CSRF の攻撃が筒抜けになってしまうので、以降で対策します。

CSRF対策

CSRF(クロスサイトリクエストフォージェリ)

CSRFについては、こちらの記事内の説明が特に分かりやすかったです。

Cookie の性質を利用した攻撃と Same Site Cookie の効果

  • ユーザーが、あるサイト(ターゲット)にログインしたままにしている
  • 別の罠サイト内に、そのターゲットサイトへのリクエスト(何かを変更するなど)を偽装して送る仕掛けがある
  • ユーザーが罠サイトを訪れ、偽装リクエストが送られる
  • ユーザーはターゲットサイトにログインしているため、そのサイトは、偽装リクエストを正常なリクエストとして受け取ってしまう

CSRFとCookie

CSRFは、ターゲットサイトに紐づくCookieにログイン状態が保存されていて、 リクエストがどこから送られたかに関係なく、Cookieが自動的に送信されてしまう ことが利用されています。

なぜ、自サイト内からのリクエストでのみ、Cookieが送信されるようになっていないのでしょうか?

もしそうだった場合、別のサイトからのリンクでターゲットサイトに訪れた時、ログインしていても、Cookieが送られないため、ログインしていないことになってしまいます。そしてリロードすると、自サイト内からのリクエストとしてCookieが送られ、ログインした状態になるという不自然な挙動になります。

RailsのCSRF対策

サーバーサイドレンダリングをするRailsアプリでは、デフォルトでフォームにトークンが仕込まれ、送られてきたリクエストのトークンをチェックすることでCSRF対策をしています。

参考: Rails セキュリティガイド

APIモードでは、フォームにトークンを仕込むことはできないので、別の方法で対策する必要があります。

実験

実際に、上で紹介したログイン機構を持つタスク管理アプリをサーバーに配置し、CSRFの実験をしてみました。

  • 下記のような偽装フォームを持つhtmlを用意し、ローカルサーバーを立ち上げます。
偽装フォーム
<form action="アプリURL" method="post">
  <input type="hidden" name="task[content]" value="偽装タスク">
  <input type="submit" value="送る">
</form>
  • 実際のアプリにログインします。
  • 同じブラウザで、偽装フォームのページを開き、「送る」を押します。
  • アプリは params[:task][:content] を受け取って、「偽装タスク」という内容のタスクを作成してしまいました!

axiosなどを使ってJSでリクエストを送る場合は、CORS設定で許可したオリジン以外は拒否されるのですが、この例のように、formからリクエストを送ると、簡単にCSRFができてしまうことが分かりました。

対策方法

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

こちらの記事で、主に3つの対策方法が紹介されていて、大変参考になりました。

今回はその中から、 固有の HTTP ヘッダを検証する方法 を用いて対策することにしました。

固有の HTTP ヘッダでCSRF対策

リクエストに X-Requested-With: XMLHttpRequest というヘッダを持たせて、バックエンド側では、そのヘッダを含まないリクエストを拒否するという方法です。

この X-Requested-With: XMLHttpRequest は一般的に使われているというだけで、ヘッダ自体には意味がなく、 固有のHTTPヘッダ をつけることに意味があります。それは、

HTML フォーム送信に関しては,一切の余分な HTTP ヘッダの付与が許可されていない。

という制約があるため、上で実験したようにフォームを偽装しても、 X-Requested-With ヘッダをリクエストに含めることができないからです。

したがって、このヘッダを持たない偽装リクエストはバックエンドで弾かれるようになります。

バックエンド側の実装

application_controller に、ヘッダをチェックする before_action を追加します。 X-Requested-With ヘッダの有無をチェックする request.xhr? というメソッドを利用します。

application_controller.rb
class ApplicationController < ActionController::API
  before_action :check_xhr_header

  private

  def check_xhr_header
    return if request.xhr?

    render json: { error: 'forbidden' }, status: :forbidden
  end
end

フロントエンド側の実装

axiosなどでリクエストする際に、 'X-Requested-With': 'XMLHttpRequest' のヘッダを含めます。

axios({
  method: 'HTTPメソッド',
  url: '送信先URL',
  withCredentials: true,
  headers: { 'X-Requested-With': 'XMLHttpRequest' }, // 追加
  data: { 送信データ },
})

以上の実装で、上で行ったCSRF実験で、リクエストを送っても forbidden が返ってくるようになりました。

まとめ

今回実装したログイン方法は、普通のRailsアプリと同じようにsessionを使えるので、初めてのWeb API開発としては、とっつきやすい方法だと思いました。

CSRFの対策は、必要最低限のレベルだと思います。実際のアプリ開発で、もっとセキュリティを高めるために、Cookieの SameSite 属性を組み合わせるなどの方法を学びたいです。

参考記事

SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜

JWT・Cookieそれぞれの認証方式のメリデメ比較

HTML5のLocal Storageを使ってはいけない

SPA + Rails API 構成におけるcookie + session認証

Rails の API モードでセッションを有効にする

rails-apiでcookieを使う

Cookie の性質を利用した攻撃と Same Site Cookie の効果

Rails セキュリティガイド

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

このWeb APIってCSRF対策出来てますか?って質問にこたえよう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】Missing host to link to! Please provide the :host parameterが出た

自作アプリRailsチュートリアル方式でログイン機構とパスワードリセット機構を実装した時の話です。

11章のaccount activation機能を無視して12章を進めていました。

具体的にリスト12.10まで進めたところで、試しに再設定用ページからメール送信ボタンをクリックしたところ、
タイトルのエラーが出てしまいました。

Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true

「ホストへのリンクがないよー」といっているのはわかりますが、そのホストとは何のことを指しているのか全くわからなかったためググったところ、テストメールに使うドメインホストが抜けてるとのことでした。

config/environments/test.rb
config.action_mailer.default_url_options = { host: 'example.com' }

(※実際のホスト名は自作アプリ向けに変えてます)
でいけました!ここ11章できちんと説明されているのに飛ばしたからハマってしまいました...。

今回に限らず別のところでもよくハマるのですが、railsでアプリを作成するのに何が足りないとかなぜそのエラーがでてしまうのか、そのエラーは何を意味しているのか理解するのって難しいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsのmigration fileが外部キーでdown出来ない時の対処法

ruby 2.6.6
rails 6.0.3
catalina 10.15.5

railsのmigration fileが外部キーでdown出来ない時の対処法がググってもあまり情報が出て来なかったので、後学のために。
要らないmigration fileを削除したく以下コマンド叩く。

$ rails db:migrate:down VERTION=migration_file_id

すると下記エラー

Mysql2::Error: Cannot drop table 'stores' referenced by a foreign key constraint

エラーメッセージに外部キーと書いてあるので、まずは無効化。の前にdatabase選択
以下コマンド後、少し解説

mysql> show databases;
mysql> use database名;
# 外部キー無効化
mysql> set foreign_key_checks = 0;
mysql> show tables;
mysql> select * from schema_migrations;
mysql> select * from schema_migrations where version = 'migration_file_id';
mysql> delete  from schema_migrations where version = 'migration_file_id';
mysql> select * from schema_migrations;
$ rails db:migrate:status
$ rails db:rollback

ここまでコマンドのみ

以下解説

mysql> select * from schema_migrations;
+----------------+
| version        |
+----------------+
| 20200714085643 |
| 20200715061816 |
+----------------+
2 rows in set (0.00 sec)

消したいのはこれ → 20200714085643

mysql> select * from schema_migrations where version = '20200714085643';
+----------------+
| version        |
+----------------+
| 20200714085643 |
+----------------+
1 row in set (0.00 sec)
mysql> delete  from schema_migrations where version = '20200714085643';
Query OK, 1 row affected (0.00 sec)
mysql> select * from schema_migrations;
+----------------+
| version        |
+----------------+
| 20200715061816 |
+----------------+
1 row in set (0.01 sec)
# 20200714085643が消えた
# 外部キー有効化
mysql> set foreign_key_checks = 1;
# 最新のマイグレーションファイルのバージョンが指定データベースのschema_migrationsテーブルから削除される
$ rails db:rollback

downしたいファイルが複数ある場合は下記コマンド

# 5つのマイグレーションファイルをロールバックしたい時
$ rails db:rollback STEP=5

これでrails db:migrate:statusすればdownになっているはず。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動画の再生時間(HH:MM:SS)を秒数にしたいし、その逆もしたい

HH:MM:SSを秒に

# rails c または以下をrequire
require "active_support/duration"

duration_text = "00:01:00"
Time.parse(duration_text) - Time.parse("00:00:00") # => 60.0

秒をHH:MM:SSに

Time.at(60).utc.strftime("%H:%M:%S") # => "00:01:00"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeでアプリのコンテナが立ち上がらず困惑しました!というお話

ある日、開発途中のrailsアプリを立ち上げようと

docker-compose up

しようとしたら、

Cannot start service webpacker: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"./bin/webpack-dev-server\": stat ./bin/webpack-dev-server: no such file or directory": unknown

というエラーでて、
アプリのコンテナが、extend with 0で落ちました。

その後、エラー文を調べると以下記事を見つけ解決にいたりました!
docker-compose upしようとしたら「Cannot start service」 - Qiita

結果からいうと
以前、gitignoreを編集しているときに
bin/以下ファイルを何を思ったか追加してて、ディレクトリごと削除していたんです。

で、
docker-composeファイル内のcommandで指定している"bin/webpack-dev-server"コマンドを実行しようとしても、そもそもdocker-composeでマウントするディレクトリにないですけど!
と注意を受けていた訳でした。

原因がわかったので、解決

git log
でbinファイルを削除してしまったコミットIDを調べる。

git revert [コミットID]
コミットを取り消す

docker-compose up
無事起動を確認

慢心ですね。
gitignoreの扱いには注意。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]

前提

あるショップのデータを作成するときに、そのショップへのコメントも一気に作成したいと思います。前提となる環境は以下の通りです。

  • Rails 5.2.4.2
  • rspec-rails 4.0.1

テーブルについて

テーブルの構造はこんな感じです。

親テーブル(shops)

shops
id
name

子テーブル(comments)

comments
id
shop_id
content

モデル

親モデル、子モデルのmodelファイルはそれぞれこのようになっています。

親モデル(Shop)

models/shop.rb
class Shop < ApplicationRecord
  has_many :comments
  accepts_nested_attributes_for :comments
end

親モデルにてaccepts_nested_attributes_for :commentsを指定し、子モデルのデータを受け入れるようにしておくことがポイントです。

子モデル(Comment)

子モデルの記載には特に変わったことはありません。

models/comment.rb
class Comment < ApplicationRecord
  belongs_to :shop
end

コントローラー

親モデル側のコントローラーに、工夫すべき点がいくつかあります。

controllers/shops_controller.rb
class ShopsController < ApplicationController
  def new
    @shop = Shop.new
    @shop.comments.build # ←一つ目のポイント
  end

  def create
    @shop = Shop.create(shop_params)
    if @shop.save
      # 成功したときの処理
    else
      # 成功しなかったときの処理
    end
  end

  private

  def shop_params # ↓2つ目のポイント
    params.require(:shop).permit(:name, comments_attributes: [:content])
  end

end

一つ目のポイントは、newアクションのところで、@shop.comments.buildと記載し、子テーブルであるcommentsのインスタンスをbuildまでしておくことです。こうすることで、親テーブルに紐づくcommentsを作成する準備ができます。

二つ目のポイントは、shop_paramsのところで、comments_attributes: [:content]と記載し、子テーブルの要素もpermitしておくことです。要素が二つ以上あるときには、comments_attributes: [:content, :attr2, :attr3]のように記載します。

ビュー

ビューには、このように記載します。

views/shops/new.rb
= form_with model: @shop, local: true do |f|
  = f.label :name, "店名"
  = f.text_field :name

  = f.fields_for :comments, local: true do |comments_form|
   = comments_form.label :content, "ショップへのコメント"
  = comments_form.text_field :content

= f.submit "送信"

これで、子テーブルのデータも一気に作成されるはずです。
余談ですが、以前f.fields_forのところをfields_forと書いてしまい、子テーブルの要素がshop_paramsとして送られずはまったことがありました。。。

f.fields_forと記載することで、親テーブルのフォームオブジェクトに紐づけることができます。

テスト

テストもそれなりにコツが必要だったので記載します。ここでは、リクエストspecのみを書きます。

factory

親(shop)

spec/factories/shops.rb
FactoryBot.define do
  factory :shop do
    name { "テストのお店" }

    trait :with_nested_instances do
      after( :create ) do |shop|
        create :comment, id: shop.id
      end
    end  
  end
end

trait といのは、FactoryBotで、状況により少しだけ違うデータを用いたい場合の設定方法です。

after(:create)はFactoryBotのコールバックです。この辺りはこちらの記事が参考になりました。

子(comment)

spec/factories/comments.rb
FactoryBot.define do
  factory :comment do
    content { "コメント" }
  end
end

子モデルのfactoryに特に変わった点はありません。

リクエストspec

spec/requests/shops_request_spec.rb
RSpec.describe "Shops", type: :request do

  describe "POST /shops" do
    context "依存関係のあるテーブルのparamsが送信されているとき" do
    before do
        @comment_params = {
          comments_attributes: {
            "0": FactoryBot.attributes_for(:comment)
          }
        }
        @params_nested = { 
          shop: FactoryBot.attributes_for(:shop).merge( @comment_params )
        }
      end

      it 'リクエストが成功すること' do
        post shops_url, params: @params_nested
        expect(response.status).to eq 302
      end

      it "ショップ情報とコメントが新規作成されること" do
        expect do
          post shops_url, params: @params_nested
        end.to change(Shop, :count).by(1) and change(Comment, :count).by(1)
      end
    end
  end
end

"0"あたりがちょっと特殊な書き方に見えますが、これは、実際のデータ送信時に送られるparamsに合わせた形になります。長かったですが、テストまで頑張ったおかげで、依存関係のあるデータの作成と大分仲良くなれました。

参考記事

ここまでに参考にした記事はこちらです。
RSpecにおけるFactoryGirlの使い方まとめ
【Rails】複数のレコードを作成する。modelの関係性によって異なるform_for / fields_forの使い方
↓特にこの記事にはお世話になりました!!
[Rails]accepts_nested_attributes_forの使い方

追記(発展学習?)

結局は自分の凡ミスだったのですが、同様にネストしたテーブルについて悩んていたときにスクールの同窓生コミュニティに投稿したら、以下のことを教えていただきました。

▼よくわからないがフォームがすっきり書けるらしい(そんな理解ですみません)
【Rails】FormObjectを使ってほしい

▼次のフォームには使ってみたいと思っているgem, 1体多の構造を持つテーブルで、多数の子テーブルデータが一気に作成できるやつ
fields_forを使った子モデルへの複数レコード保存【cocoonが便利】

では、今日もこれから仕事、頑張ります。アドバイスくださった皆様、ありがとうございました!!:relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HerokuでMongoDBのアドオンのmLabが終了するので、別のMongoBD無料サービスにマイグレーションした時のメモ

概要

Herokuで無料で利用可能なMongoDBのアドオンであるmLabのサービス終了に伴い、後継の無料利用可能なMongoDBサービス(MongoDB Atlas)に移行した際の方法のメモです。

画面操作手順が多く、一部の手順等を省略しておりますが、移行時の参考になればと思います。

背景

Heroku上で無料プランのみで、Railsで作成した趣味のWebサイト(※)を構築しており、
その中でMongoDBが無料利用可能なmLabアドオンを使用していました。
無料プランですと容量等、色々と制限がありますが、小規模で使用する分には問題ない形で使用できていました。

※作成したWEBサイト:駿河部

ですが、7/11頃に下記のようなメールが届いていました。

mail

要約すると、2020年の11月10日にmLabのMongoDBのアドオンは削除されて使用できなくなるという事となります。

詳細は下記に記載されていますが、mLab自体がMongoDB本体の会社(MongoDB Inc)に買収されたことに伴い、そちらのクラウドサービス(MongoDB Atlas)に統合されるためのようです。
参考

幸いにもサービス停止に伴い、MongoDB Atlas側へ無料利用可能なプラン(512MBまで利用可能)へのマイグレーション方法が案内されており、今回はそちらのプランへのマイグレーションを実施した際の記録になります。

手順

MongoDBの対応バージョンの確認

mLabで動いていたMongoDBのバージョンは3.6ですが、MongoDB Atlasは4.2となるため、使用しているmongoidやrubyのバージョンに注意が必要です。
今回私の場合には4.2に対応しているバージョンを使用しており、幸いにも対応は不要でした。
バージョン等の対応に関しては下記が参考になります。
参考

アカウント作成

https://www.mongodb.com/cloud/atlas
にアクセスして、Atlasアカウントを作成します。
(詳細は割愛しますが、メールアドレスやパスワードを入力し、メールのアクティベーションを実施してください。)
スクリーンショット 2020-07-20 21.21.18.png

Clusterを作成

ログイン後、クラスターを作成します。
Clusters → Build a Clusterを選択します。

create.png

Freeプランを選択します。

Free.png

AWS, GCP, Azureやリージョン等の選択ができますのでお好みで選択してください。
reagion.png

Clusterが作成できれば完了です。

clu2.png

mLabアカウントとの接続

画面左上の歯車アイコンを選択します。

setting.png

Connect to mLabといった項目があるので右側のボタンを選択します。

mLab

mLabのログイン画面に移動するのでログインします。
mlab.png

なお、mLabのアカウントが不明な場合には一度Herokuにログインして、mLabを使用しているアプリ内のAddonsのmLabを選択するとログインできるのでその後、上記のログイン画面でリロードするとログインできます。

heroku.png

ログイン完了後に、ダイアログが表示されるのでAutorizeで連携を許可します。
auth.png

Atrasの画面に戻って、mLab Accountの画面が表示されれば連携が完了です。

mLabCon.png

マイグレーション設定

mLab Accountの画面でActions → ... → Configure Migrationを選択します。

mlabcon1.png

Create or Select a projectを選択します。
cop.png

プロジェクトを選択して Confirm ~ を選択します。
(デフォルトであればProject 0)
t1.png

mLabから引き継いだアカウント情報などが表示されるので確認及び、importにチェックがついていることを確認して、 Import ~ を選択します。
import.png

Allow ~ のチェックボックスにチェックを入れて、右下のボタンを選択します。

Allow.png

作成したClusterを選択して、右下のボタンを選択します。

clus.png

マイグレーションの設定が完了しました。
fin1.png

マイグレーション実行

Migrate DeploymentのConfirmを選択します。

mig1.png

Confirm And Continueを選択します。

mig2.png

チェックボックスにチェックを入れずに、Begin Test Runを選択します。
mig3.png

マイグレーションが始まるので、完了するまで待ちます。
mig4.png

マイグレーションが無事に成功したら、Confirm Connectivityを選択します。
mig5.png

Confirm ~ を選択します。
mig55.png

チェックボックスにチェックを入れて、Begin Migrationを選択します。
(下記では、アプリケーションを事前に止めるように注意しているのでheroku ps:scale web=0等のコマンドで予めアプリを止めた方が無難です。)
mig6.png

マイグレーションが始まるので待ちます。
しばらくすると完了するので、Start Using Atlasを選択します。

migfin.png

MongoDBの接続URLの情報の雛形が表示されるので、コピーした上で
username及びpasswordに関してはherokuで使用していた値に置き換えて使用します。
con.png

herokuの環境変数を使用している場合には、下記のように環境変数を新しい値に書き換えます。
heroku config:set 環境変数名="上記の接続URL"

アプリの起動

Herokuのアプリを止めている場合には、再立ち上げを行います。
MongoDBを使用している機能が動くこと確認してください。

後始末

HerokuのmLabのアドオンを消します。
下記等を参考に予めバックアップを取ることが推奨されているので任意で実施してください。
https://docs.mlab.com/backups/#retention-policies

また、mLabを削除すると自動で追加されたMONGODB_URI"や"MONGOLAB_URI等の環境変が削除されるとの事ですので、
heroku config等で環境変数の値を出力してバックアップしておく事が推奨されています。

Herokuにログインを行い、Configure Add-onsを選択します。
herokuaddons.png

mLabのDelete Add onを選択します。
確認ダイアログが表示されるのでアプリ名を入力して削除します。
delmongo.png

以上でHeroku側のmLabの削除が完了しました。
念のためMongoDBを使用した機能が動くか確認してみましょう。

まとめ

Heroku上で動くMongoDBのアドオンのmLabの終了に伴い、代替サービスとなるMongoDB Atlasへ移行時の方法のメモを紹介しました。

手順が多く大変でしたが、無事マイグレーションできたので一安心しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js Rails ページ遷移

前提

Rails 5.2
Ruby 2.5.1

package.json
{
  "name": "app",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "5.1.1",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.11"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

ファイル構成

app
|--javascript
   |--packs
      |--hello_vue.js
   |--app.vue

本題

かなり初歩的ですが、とても躓いたため記述しました。

・VueRouterを導入

$ npm install vue-router

・app.vueにを追記

app.vue
<template>
  <div id="app">
    <!-- 追記 -->
    <RouterView />
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      message: "Hello Vue!",
    };
  }
};
</script>

・app/javascript/packsの中にpagesフォルダー作成
・作成したpagesフォルダーのなかに、ファイルを2つ作成。

pages/Hoge.vue
<template>
  <div>
    <router-link to="/">Test</router-link>
  </div>
</template>
pages/test.vue
<template>
  <div>
    <router-link to="/hoge">Hoge</router-link>
    <p>login</p>
  </div>
</template>

・packsフォルダーにrouter.jsを作成

router.js
import Vue from "vue";
import VueRouter from "vue-router";

import Test from "./pages/Test.vue";
import Hoge from "./pages/Hoge.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    component: Test,
  },
  {
    path: "/Hoge",
    component: Hoge,
  },
];

const router = new VueRouter({
  mode: "history",
  routes,
});

export default router;

・hello_vue.jsにvue-routerを追記

hello_vue.js
import Vue from "vue";
import router from "./router";
import App from "../app.vue";

document.addEventListener("DOMContentLoaded", () => {
  const app = new Vue({
    router,
    components: { App },
    template: "<App />",
    render: (h) => h(App),
  }).$mount();
  document.body.appendChild(app.$el);
});

ビルド後にページリロードして動作確認。

# bin/webpack-dev-server
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む