学生向けプログラミング入門

学生向けにプログラミングを解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails

Ruby on Rails 6.0によるWebアプリケーション開発33 ログイン(セッション)

<<前  [TOP]  次>>


認証されたユーザがアプリケーションの全ての管理機能を使えるようにします。
まず管理用のユーザ名とパスワードを格納するモデルとデータベーステーブルの作成から始めます。
コマンドプロンプトで「bin」フォルダに移動して「rails generate scaffold User name:string password:digest」と入力します。

ユーザモデルの作成
ユーザモデルの作成


途中「scaffolds.scss」を上書きするか聞いてくるので「n」と入力して上書きしないようにします。
スタイルシートを上書きしない
スタイルシートを上書きしない


続いてマイグレーションを実行します。
コマンドプロンプトで「rails db:migrate」と入力してください。
マイグレーションの実行
マイグレーションの実行


ユーザモデルに具体的な内容を記述していきます。
「C:\Rails6\work\shop\app\models\user.rb」ファイルを以下のように編集します。


【C:\Rails6\work\shop\app\models\user.rb】

class User < ApplicationRecord
  validates :name, presence: true, uniqueness: true
  has_secure_password
end



名前が存在してユニークであること、つまり同じ名前のユーザがデータベースに2人いないかを調べます。
「has_secure_password」はパスワードとその確認フィールドをフォームに表示する機能や指定された名前とパスワードでユーザ認証する機能を実現します。


パスワードをハッシュ化させるために「bcrypt」というgemをインストールします。
まず「Gemfile」に「gem 'bcrypt', '~> 3.1.7'」と記述を追加します。


【C:\Rails6\work\shop\Gemfile】

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.6'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.2', '>= 6.0.2.2'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.4.4'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
end

group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of web drivers to run system tests with browsers
  gem 'webdrivers'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem 'rails-i18n'

gem 'bcrypt', '~> 3.1.7'



コマンドプロンプトで「bundle install」を行いgemを追加します。

「bundle install」
「bundle install」


「bcrypt」の追加
「bcrypt」の追加


「users_controller.rb」ファイルの編集を行います。
作成(create)の操作を行ったあとにユーザの表示(show)にリダイレクトするのを避けるようにします。
その代わりユーザのindexにリダイレクトしてユーザ名をフラッシュの表示に追加します。
「create()」「update()」「index()」の3つのメソッドの記述を変更しています。


【C:\Rails6\work\shop\app\controllers\users_controller.rb】

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    @users = User.order(:name)
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = User.new
  end

  # GET /users/1/edit
  def edit
  end

  # POST /users
  # POST /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to users_url, notice: "ユーザ #{@user.name}を作成しました。" }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to users_url, notice: "ユーザ #{@user.name}を更新しました。" }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'ユーザを削除しました。' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:name, :password, :password_confirmation)
    end
end



次は「_form.html.erb」ビューの編集を行います。


【C:\Rails6\work\shop\app\views\users\_form.html.erb】

<div class="depot_form">

<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %>
        prohibited this user from being saved:</h2>

      <ul>
        <% user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <h2>Enter User Details</h2>

  <div class="field">
    <%= form.label :name, 'Name:' %>
    <%= form.text_field :name, size: 40 %>
  </div>

  <div class="field">
    <%= form.label :password, 'Password:' %>
    <%= form.password_field :password, size: 40 %>
  </div>

  <div class="field">
    <%= form.label :password_confirmation, 'Confirm:' %>

    <%= form.password_field :password_confirmation,
                            id: :user_password_confirmation,
                            size: 40 %>
    
  </div>

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

</div>



「new.html.erb」ビューも変更しておきます。


【C:\Rails6\work\shop\app\views\users\new.html.erb】

<h1>新規ユーザの作成</h1>

<%= render 'form', user: @user %>

<%= link_to 'Back', users_path %>



ブラウザで「http://localhost:3000/users/new」にアクセスして確認してみます。

ブラウザの表示
ブラウザの表示


フォームに入力してユーザを作成してみます。
ユーザの作成
ユーザの作成


MySQLに接続してデータベース内を調べてみると、ユーザの詳細が格納されたことが確認できます。
格納されたユーザの詳細確認
格納されたユーザの詳細確認


作成したユーザを使ってユーザーの認証できるように実装していきます。
作成するのはログインとログアウトのためのセッションコントローラと管理者を迎えるためのコントローラです。
コマンドプロンプトで「bin」フォルダに移動して「rails generate controller Sessions new create destroy」と入力してセッションコントローラを作成します。
Sessionコントローラの作成
Sessionコントローラの作成


もう一つ「rails generate controller Admin index」と入力して管理者を迎えるためのコントローラ「Admin」を作成します。
Adminコントローラの作成
Adminコントローラの作成


Sessionsコントローラのcreateアクションは管理者がログイン済みであることを示す情報をsessionに記録する必要があります。
「:user_id」をキーとしてUserオブジェクトのidを格納します。
また、ログアウトをしたとき用に「destroy()」メソッドも実装しておきます。
「sessions_controller.rb」ファイルを以下のように編集します。


【C:\Rails6\work\shop\app\controllers\sessions_controller.rb】

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(name: params[:name])
    if user.try(:authenticate, params[:password])
      session[:user_id] = user.id
      redirect_to admin_url
    else
      redirect_to login_url, alert: "無効なユーザー/パスワードの組み合わせです。"
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to market_index_url, notice: "ログアウトしました。"
  end
end



セッションの「new」アクション用のビューを編集します。


【C:\Rails6\work\shop\app\views\sessions\new.html.erb】

<section class="shop_form">
  <% if flash[:alert] %>
    <aside class="notice"><%= flash[:alert] %></aside>
  <% end %>

  <%= form_tag do %>
    <h2>ログインしてください。</h2>
    <div class="field">
      <%= label_tag :name, '名前:' %>
      <%= text_field_tag :name, params[:name] %>
    </div>

    <div class="field">
      <%= label_tag :password, 'パスワード:' %>
      <%= password_field_tag :password, params[:password] %>
    </div>

    <div class="actions">
      <%= submit_tag "ログイン" %>
    </div>
  <% end %>
</section>



indexページを追加しておきます。
管理者がログイン後に最初に目にする画面です。


【C:\Rails6\work\shop\app\views\admin\index.html.erb】

<h1>ようこそ</h1>

<p>
  現在時刻は<%= Time.now %>です。
  現在<%= pluralize(@total_orders, "件") %>の注文があります。
</p>



このコードを実際に使えるようにするにはもう一つ作業が残っています。
これまではモデルとルートの生成をscaffoldに任せていましたが、今回のコントローラにはデータベースに基づいたモデルがないのでコントローラだけを作成しました。
このコントローラに関しては従うべきscaffoldの規約がないためGETリクエストに応答すべきアクションもわからなければPOSTリクエストに応答すべきアクションもわからないといったことになります。
このような情報を指定するには「config\routes.rb」ファイルを編集します。


【C:\Rails6\work\shop\config\routes.rb】

Rails.application.routes.draw do

  get 'admin' => 'admin#index'
  controller :sessions do
    get  'login' => :new
    post 'login' => :create
    delete 'logout' => :destroy
  end

  get 'sessions/new'
  get 'sessions/create'
  get 'sessions/destroy'
  resources :users
  resources :orders
  resources :line_items
  resources :carts
  #get 'market/index'
  root 'market#index', as: 'market_index'
  resources :goods
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end



ブラウザで「http://localhost:3000/admin」にアクセスすると、管理者用のインターフェイスが開けるようになりました。

管理者用インターフェイス
管理者用インターフェイス


管理者としてログインしたユーザ以外はサイトの管理ページにアクセスできないようアクセス制限をかけます。
これはRailsのフィルタ機能を使うと簡単に実装できます。
Railsではフィルタによってアクションメソッドの呼び出しを補足し、その呼出しが実行される直前や呼び出しから復帰した直後に独自の処理を加えることができます。
まずは「application_controller.rb」ファイルの実装です。


【C:\Rails6\work\shop\app\controllers\application_controller.rb】

class ApplicationController < ActionController::Base
  before_action :authorize

  protected

    def authorize
      unless User.find_by(id: session[:user_id])
        redirect_to login_url, notice: "ログインしてください。"
      end
    end
end



「before_action :authorize」によってこのアプリケーションのすべてのアクションの前に「authorize()」メソッドが呼び出されるようになります。
しかしこれではストア自体へのアクセスを管理者だけに制限してしまいます。
認証を必要としないメソッドやコントローラに「skip_before_action()」メソッドの呼び出しを追加します。
「market_controller.rb」「sessions_controller.rb」「carts_controller.rb」「line_items_controller.rb」「orders_controller.rb」の5つです。


【C:\Rails6\work\shop\app\controllers\market_controller.rb】

class MarketController < ApplicationController

  skip_before_action :authorize

  include CurrentCart
  before_action :set_cart

  def index
	@goods = Good.order(:title)
  end
end



【C:\Rails6\work\shop\app\controllers\sessions_controller.rb】

class SessionsController < ApplicationController

  skip_before_action :authorize

  def new
  end

  def create
    user = User.find_by(name: params[:name])
    if user.try(:authenticate, params[:password])
      session[:user_id] = user.id
      redirect_to admin_url
    else
      redirect_to login_url, alert: "無効なユーザー/パスワードの組み合わせです。"
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to market_index_url, notice: "ログアウトしました。"
  end
end



【C:\Rails6\work\shop\app\controllers\carts_controller.rb】

class CartsController < ApplicationController

  skip_before_action :authorize, only: [:create, :update, :destroy]

  before_action :set_cart, only: [:show, :edit, :update, :destroy]
  rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart

  # GET /carts
  # GET /carts.json
  def index
    @carts = Cart.all
  end

  # GET /carts/1
  # GET /carts/1.json
  def show
  end

  # GET /carts/new
  def new
    @cart = Cart.new
  end

  # GET /carts/1/edit
  def edit
  end

  # POST /carts
  # POST /carts.json
  def create
    @cart = Cart.new(cart_params)

    respond_to do |format|
      if @cart.save
        format.html { redirect_to @cart, notice: 'カートを作成しました。' }
        format.json { render :show, status: :created, location: @cart }
      else
        format.html { render :new }
        format.json { render json: @cart.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /carts/1
  # PATCH/PUT /carts/1.json
  def update
    respond_to do |format|
      if @cart.update(cart_params)
        format.html { redirect_to @cart, notice: 'カートが更新されました。' }
        format.json { render :show, status: :ok, location: @cart }
      else
        format.html { render :edit }
        format.json { render json: @cart.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /carts/1
  # DELETE /carts/1.json
  def destroy
	@cart.destroy if @cart.id == session[:cart_id]
	session[:cart_id] = nil
	respond_to do |format|
		format.html { redirect_to market_index_url, notice: 'カートが空になりました。' }
		format.json { head :no_content }
	end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_cart
      @cart = Cart.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def cart_params
      params.fetch(:cart, {})
    end

	def invalid_cart
		logger.error "無効なカート(#{params[:id]})にアクセスしようとしました。"
		redirect_to market_index_url, notice: '無効なカートです。'
	end

end



【C:\Rails6\work\shop\app\controllers\line_items_controller.rb】

class LineItemsController < ApplicationController

  skip_before_action :authorize, only: :create

  include CurrentCart
  before_action :set_cart, only: [:create]
  before_action :set_line_item, only: [:show, :edit, :update, :destroy]

  # GET /line_items
  # GET /line_items.json
  def index
    @line_items = LineItem.all
  end

  # GET /line_items/1
  # GET /line_items/1.json
  def show
  end

  # GET /line_items/new
  def new
    @line_item = LineItem.new
  end

  # GET /line_items/1/edit
  def edit
  end

  # POST /line_items
  # POST /line_items.json
  def create

	good = Good.find(params[:good_id])
	@line_item = @cart.add_good(good)

    respond_to do |format|
      if @line_item.save
        format.html { redirect_to market_index_url }
	format.js { @current_item = @line_item }
        format.json { render :show, status: :created, location: @line_item }
      else
        format.html { render :new }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /line_items/1
  # PATCH/PUT /line_items/1.json
  def update
    respond_to do |format|
      if @line_item.update(line_item_params)
        format.html { redirect_to @line_item, notice: '品目が更新されました。' }
        format.json { render :show, status: :ok, location: @line_item }
      else
        format.html { render :edit }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /line_items/1
  # DELETE /line_items/1.json
  def destroy
    @line_item.destroy
    respond_to do |format|
      format.html { redirect_to line_items_url, notice: '品目を破棄しました。' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_line_item
      @line_item = LineItem.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def line_item_params
      params.require(:line_item).permit(:good_id, :cart_id)
    end
end



【C:\Rails6\work\shop\app\controllers\orders_controller.rb】

class OrdersController < ApplicationController

  skip_before_action :authorize, only: [:new, :create]

  include CurrentCart
  before_action :set_cart, only: [:new, :create]
  before_action :ensure_cart_isnt_empty, only: :new
  before_action :set_order, only: [:show, :edit, :update, :destroy]

  # GET /orders
  # GET /orders.json
  def index
	@orders = Order.all
  end

  # GET /orders/1
  # GET /orders/1.json
  def show
  end

  # GET /orders/new
  def new
    @order = Order.new
  end

  # GET /orders/1/edit
  def edit
  end

  # POST /orders
  # POST /orders.json
  def create
    @order = Order.new(order_params)
    @order.add_line_items_from_cart(@cart)


    respond_to do |format|
      if @order.save
	Cart.destroy(session[:cart_id])
	session[:cart_id] = nil
        OrderMailer.received(@order).deliver_later
        format.html { redirect_to market_index_url, notice: 'ご注文ありがとうございました。' }
        format.json { render :show, status: :created, location: @order }
      else
        format.html { render :new }
        format.json { render json: @order.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /orders/1
  # PATCH/PUT /orders/1.json
  def update
    respond_to do |format|
      if @order.update(order_params)
        format.html { redirect_to @order, notice: 'Order was successfully updated.' }
        format.json { render :show, status: :ok, location: @order }
      else
        format.html { render :edit }
        format.json { render json: @order.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /orders/1
  # DELETE /orders/1.json
  def destroy
    @order.destroy
    respond_to do |format|
      format.html { redirect_to orders_url, notice: 'Order was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_order
      @order = Order.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def order_params
      params.require(:order).permit(:name, :address, :email, :pay_type)
    end

    def ensure_cart_isnt_empty
	if @cart.line_items.empty?
		redirect_to market_index_url, notice: 'カートは空です。'
	end
    end

    def pay_type_params
      if order_params[:pay_type] == "クレジットカード"
        params.require(:order).permit(:credit_card_number, :expiration_date)
      elsif order_params[:pay_type] == "現金"
        params.require(:order).permit(:routing_number, :account_number)
      elsif order_params[:pay_type] == "着払い"
        params.require(:order).permit(:po_number)
      else
        {}
      end
    end
end



これでストア内のアクセスは制限されず、管理ページのみアクセス制限がかかるようになりました。
http://localhost:3000/carts/」にアクセスすると「http://localhost:3000/login」にリダイレクトされます。

ブラウザの表示
ブラウザの表示


ログインすれば「http://localhost:3000/orders/」にアクセスできるようになります。


<<前  [TOP]  次>>