↓↓クリックして頂けると励みになります。
【23 | Gメール】 << 【ホーム】 >> 【25 | ユーザー管理】
作成したユーザーがログインすることでユーザーの管理機能を使えるように実装します。
まず管理用のユーザ名とパスワードを格納するモデルとデータベーステーブルの作成から始めます。
Digestとは、ハッシュ値を作成するためのモジュールです。
Digestを使えば、ハッシュ値を生成することができます。
ハッシュ値とは、変換して得られるデータです。
データを一方向にしか変換できないのが特徴で、ハッシュ値から元のデータを復元したり、推測したりできないようになっています。
これはパスワードの保存などで用いられます。
ターミナルで以下のコマンドを入力してください。
コマンド
rails generate scaffold User name:string password:digest
~/Desktop/Rails7_1/SampleCart $ rails generate scaffold User name:string password:digest invoke active_record create db/migrate/20240125013726_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb create app/views/users/_user.html.erb invoke resource_route invoke test_unit create test/controllers/users_controller_test.rb create test/system/users_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder create app/views/users/_user.json.jbuilder
続いてマイグレーションを実行します。
コマンドプロンプトで以下のコマンドを入力してください。
コマンド
rails db:migrate
~/Desktop/Rails7_1/SampleCart $ rails db:migrate == 20240125013726 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0396s == 20240125013726 CreateUsers: migrated (0.0398s) =============================
パスワードをハッシュ化させるために「bcrypt」というgemをインストールします。
「Gemfile」に「gem 'bcrypt', '~> 3.1.7'
」と記述を追加するか、36行目のコメントアウトを削除します。
今回は36行目のコメントアウトを削除しました。
記述追加 【SampleCart/Gemfile】36行目
source "https://rubygems.org" ruby "3.1.2" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.1.2" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails" # Use postgresql as the database for Active Record gem "pg", "~> 1.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] gem "importmap-rails" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] gem "turbo-rails" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] gem "stimulus-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" # Use Redis adapter to run Action Cable in production gem "redis", ">= 4.0.1" # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[ mswin mswin64 mingw x64_mingw jruby ] # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ] end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] gem "web-console" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" # Speed up commands on slow machines / big apps [https://github.com/rails/spring] # gem "spring" gem "error_highlight", ">= 0.4.0", platforms: [:ruby] end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem "capybara" gem "selenium-webdriver" end gem 'bootstrap', '~> 5.3', '>= 5.3.2' gem 'sassc-rails', '~> 2.1', '>= 2.1.2' #日本語化 gem 'rails-i18n', '~> 7.0', '>= 7.0.8'
バンドルインストールを行います。
ターミナルで以下のコマンドを入力してください。
コマンド
bundle install
モデル
作成されたユーザモデルを編集します。
「SampleCart/app/models/user.rb」ファイルを以下のように編集します。
4行目にバリデーションの記述を追加しています。
名前が存在してユニークであること、つまり同じ名前のユーザがデータベースに2人いないかを調べます。
「has_secure_password」はパスワードとその確認フィールドをフォームに表示する機能や指定された名前とパスワードでユーザ認証する機能を実現します。
記述編集 【SampleCart/app/models/user.rb】
class User < ApplicationRecord has_secure_password validates :name, presence: true, uniqueness: true end
コントローラー
「users_controller.rb」ファイルの編集を行います。
作成(create)の操作を行ったあとにユーザの表示(show)にリダイレクトするのを避けるようにします。
その代わりユーザのindexにリダイレクトしてユーザ名をフラッシュメッセージに追加します。
「create()」「update()」「index()」の3つのメソッドの記述を変更しています。
具体的には6, 28, 41行目の記述を変更しています。
記述編集 【SampleCart/app/controllers/users_controller.rb】
class UsersController < ApplicationController before_action :set_user, only: %i[ show edit update destroy ] # GET /users or /users.json def index @users = User.order(:name) end # GET /users/1 or /users/1.json def show end # GET /users/new def new @user = User.new end # GET /users/1/edit def edit end # POST /users or /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, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # PATCH/PUT /users/1 or /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, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 or /users/1.json def destroy @user.destroy! respond_to do |format| format.html { redirect_to users_url, notice: "User was successfully destroyed." } 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
ユーザーの認証できるようにコントローラーを作成します。
作成するのはログインとログアウトのためのセッションコントローラと管理者を設定するためのコントローラです。
まずはセッションコントローラーを作成します。
ターミナルで以下のコマンドを入力して下さい。
コマンド
rails generate controller Sessions new create destroy
~/Desktop/Rails7_1/SampleCart $ rails generate controller Sessions new create destroy create app/controllers/sessions_controller.rb route get 'sessions/new' get 'sessions/create' get 'sessions/destroy' invoke erb create app/views/sessions create app/views/sessions/new.html.erb create app/views/sessions/create.html.erb create app/views/sessions/destroy.html.erb invoke test_unit create test/controllers/sessions_controller_test.rb invoke helper create app/helpers/sessions_helper.rb invoke test_unit
Sessionsコントローラのcreateアクションは管理者がログイン済みであることを示す情報をsessionに記録します。
「:user_id」をキーとしてUserオブジェクトのidを格納します。
またログアウトできる様、21行目に「logout()」メソッドを実装します。
このコントローラーはログイン認証を必要としないため3行目に「skip_before_action()」メソッドを追加しています。
「sessions_controller.rb」ファイルを以下のように編集します。
記述編集 【SampleCart/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 end def logout reset_session @current_user = nil redirect_to markets_index_url, notice: "ログアウトしました。" end end
ログイン後のページを表示させるためのAdminコントローラーを作っておきます。
コントローラー編集の必要はありません。
コマンド
rails generate controller Admin index
管理ユーザのみがユーザ管理ページにアクセスできるよう制限をかけます。
これにはRailsのフィルタ機能を使うと簡単に実装できます。
「application_controller.rb」ファイルに記述を追加することで、すべてのコントローラーに適用することができます。
記述変更 【SampleCart/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()」メソッドの呼び出しを追加します。
「sessions_controller.rb」は先ほど記述してありますので、その他の5つのコントローラーファイル「markets_controller.rb」「carts_controller.rb」「line_items_controller.rb」「orders_controller.rb」「users_controller.rb」に記述を追加します。
記述追加 【SampleCart/app/controllers/markets_controller.rb】3行目
class MarketsController < ApplicationController skip_before_action :authorize include CurrentCart before_action :set_cart def index @goods = Good.order(:title) end end
記述追加 【SampleCart/app/controllers/carts_controller.rb】3行目
class CartsController < ApplicationController skip_before_action :authorize, only: [:create, :update, :destroy] before_action :set_cart, only: %i[ show edit update destroy ] rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart # GET /carts or /carts.json def index @carts = Cart.all end # GET /carts/1 or /carts/1.json def show end # GET /carts/new def new @cart = Cart.new end # GET /carts/1/edit def edit end # POST /carts or /carts.json def create @cart = Cart.new(cart_params) respond_to do |format| if @cart.save format.html { redirect_to cart_url(@cart), notice: "Cart was successfully created." } format.json { render :show, status: :created, location: @cart } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @cart.errors, status: :unprocessable_entity } end end end # PATCH/PUT /carts/1 or /carts/1.json def update respond_to do |format| if @cart.update(cart_params) format.html { redirect_to cart_url(@cart), notice: "Cart was successfully updated." } format.json { render :show, status: :ok, location: @cart } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @cart.errors, status: :unprocessable_entity } end end end # DELETE /carts/1 or /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 markets_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 markets_index_url, notice: '無効なカートです。' end end
記述追加 【SampleCart/app/controllers/line_items_controller.rb】3行目
class LineItemsController < ApplicationController skip_before_action :authorize, only: :create include CurrentCart before_action :set_cart, only: [:create] before_action :set_line_item, only: %i[ show edit update destroy ] # GET /line_items or /line_items.json def index @line_items = LineItem.all end # GET /line_items/1 or /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 or /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 markets_index_url, notice: "商品をカートに追加しました。" } format.js format.json { render :show, status: :created, location: @line_item } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @line_item.errors, status: :unprocessable_entity } end end end # PATCH/PUT /line_items/1 or /line_items/1.json def update respond_to do |format| if @line_item.update(line_item_params) format.html { redirect_to line_item_url(@line_item), notice: "Line item was successfully updated." } format.json { render :show, status: :ok, location: @line_item } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @line_item.errors, status: :unprocessable_entity } end end end # DELETE /line_items/1 or /line_items/1.json def destroy @line_item.destroy! respond_to do |format| format.html { redirect_to line_items_url, notice: "Line item was successfully destroyed." } 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) end end
記述追加 【SampleCart/app/controllers/orders_controller.rb】3行目
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 or /orders.json def index @orders = Order.all end # GET /orders/1 or /orders/1.json def show end # GET /orders/new def new @order = Order.new end # GET /orders/1/edit def edit end # POST /orders or /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 markets_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 or /orders/1.json def update respond_to do |format| if @order.update(order_params) format.html { redirect_to order_url(@order), notice: "注文が変更されました。" } format.json { render :show, status: :ok, location: @order } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @order.errors, status: :unprocessable_entity } end end end # DELETE /orders/1 or /orders/1.json def destroy @order.destroy! respond_to do |format| format.html { redirect_to orders_url, notice: "注文を削除しました。" } 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 markets_index_url, notice: '商品がカートに入っていません。' end end end
ユーザ登録後にリダイレクトするリンクも合わせて修正しています。
記述編集 【SampleCart/app/controllers/users_controller.rb】3, 31, 44行目
class UsersController < ApplicationController skip_before_action :authorize, only: [:new, :create] before_action :set_user, only: %i[ show edit update destroy ] # GET /users or /users.json def index @users = User.order(:name) end # GET /users/1 or /users/1.json def show end # GET /users/new def new @user = User.new end # GET /users/1/edit def edit end # POST /users or /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to markets_index_url, notice: "ユーザ #{@user.name}を作成しました。" } format.json { render :show, status: :created, location: @user } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # PATCH/PUT /users/1 or /users/1.json def update respond_to do |format| if @user.update(user_params) format.html { redirect_to markets_index_url, notice: "ユーザ #{@user.name}を更新しました。" } format.json { render :show, status: :ok, location: @user } else format.html { render :edit, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity } end end end # DELETE /users/1 or /users/1.json def destroy @user.destroy! respond_to do |format| format.html { redirect_to users_url, notice: "User was successfully destroyed." } 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
これでユーザ管理ページ以外はアクセス制限されないようになりました。
ルート
ルートの設定をします。
「SampleCart/config/routes.rb」ファイルを編集します。
セッションの設定は以下の部分になります。
get '/login', to: 'sessions#new' get '/logout', to: 'sessions#logout' post '/login', to: 'sessions#create'
記述編集 【SampleCart/config/routes.rb】
Rails.application.routes.draw do #action cable mount ActionCable.server => '/cable' #root root 'markets#index', as: 'markets_index' # get get 'markets/index' get 'says/hello' get 'says/goodby' get 'admin', to: 'admin#index' get '/login', to: 'sessions#new' get '/logout', to: 'sessions#logout' # post post '/login', to: 'sessions#create' #resources resources :goods resources :orders resources :line_items resources :carts resources :users # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") # root "posts#index" end
ビュー
ユーザ登録用のフォームを作成します。
レンダーファイル「SampleCart/app/views/users/_form.html.erb」を以下のように編集します。
記述編集 【SampleCart/app/views/users/_form.html.erb】
<%= form_with(model: user) do |form| %> <% if user.errors.any? %> <div style="color: red"> <h2>エラーが<%= pluralize(user.errors.count, "件") %>あります。下記の項目を修正してください。</h2> <ul> <% user.errors.each do |error| %> <li><%= error.full_message %></li> <% end %> </ul> </div> <% end %> <div class="input-group mb-3 w-75"> <span class="input-group-text">ユーザ名</span> <%= form.text_field :name, class: "form-control" %> </div> <div class="input-group mb-3 w-75"> <span class="input-group-text">パスワード</span> <%= form.password_field :password, class: "form-control" %> </div> <div class="input-group mb-3 w-75"> <span class="input-group-text">パスワード(確認)</span> <%= form.password_field :password_confirmation, class: "form-control" %> </div> <div> <%= form.submit "登録", class: "btn btn-primary" %> </div> <% end %>
次にユーザ登録ビューを編集します。
「SampleCart/app/views/users/new.html.erb」ファイルを以下のように編集します。
記述編集 【SampleCart/app/views/users/new.html.erb】
<div class="container mt-4"> <div class="h4 mt-4 mb-4">ユーザ登録</div> <%= render "form", user: @user %> <br> <div> <%= link_to "登録ユーザ一覧", users_path, class: "btn btn-success" %> </div> </div>
ブラウザでユーザー登録フォームを確認します。
ブラウザ確認
http://localhost:3000/users/new
フォームにユーザ名とパスワードを入力してユーザを登録します。
Posticoでユーザーテーブルを確認してみます。
Windowsの場合はHeidiSQLで確認してください。
パスワードがハッシュ化されて保存されているのがわかります。
ログイン用のビューを編集します。
「SampleCart/app/views/sessions/new.html.erb」ファイルを以下のように編集してください。
記述編集 【SampleCart/app/views/sessions/new.html.erb】
<div class="container mt-4"> <div class="h4 mb-4"> ログイン </div> <%= form_tag do %> <div class="input-group mb-3 w-75"> <span class="input-group-text">ユーザ名</span> <%= text_field_tag :name, params[:name], class: "form-control" %> </div> <div class="input-group mb-3 w-75"> <span class="input-group-text">パスワード</span> <%= password_field_tag :password, params[:password], class: "form-control" %> </div> <div> <%= submit_tag "ログイン", class: "btn btn-primary" %> </div> <% end %> </div>
ログイン後に表示されるビューを編集します。
「SampleCart/app/views/admin/index.html.erb」ファイルを以下のように編集します。
記述編集 【SampleCart/app/views/admin/index.html.erb】
<div class="container mt-4"> <div class="h4"> ログイン中 </div> <p> 現在時刻は<%= Time.now %>です。<br/> 現在<%= pluralize(@total_orders, "件") %>の注文があります。 </p> </div>
ではログインの動作を確認します。
ログインしていない状態で以下のURLにアクセスします。
ログインしてくださいと表示され、ログインページにリダイレクトされます。
http://localhost:3000/carts/show
ログインすると、ログイン中と表示されます。
この状態(ログインしている状態)で先ほどのURLにアクセスすると、「無効なカートです」と表示される様になります。
http://localhost:3000/carts/show
>
ログアウトします。
URLに以下のアドレスを入力してください。
ログアウトしましたと表示されます。
http://localhost:3000/logout
また先ほどのURLにアクセスすると、ログインしてくださいと表示されるはずです。
http://localhost:3000/carts/show
ログインボタンとログアウトボタンを実装します。
「SampleCart/app/views/shared/_navbar.html.erb」ファイルを以下のように編集します。
記述編集 【SampleCart/app/views/shared/_navbar.html.erb】14, 26行目
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container-fluid"> <a class="navbar-brand" href="/">Smple Cart</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <!-- もしログインしていなかったら--> <li class="nav-item" style="margin-bottom: 0.1rem;"> <span style="margin-left: 3rem;"><a class="btn btn-danger">新規登録</a></span> </li> <li class="nav-item"> <span style="margin-left: 3rem;"><%= link_to "ログイン", login_path, class: "btn btn-success text-light" %></span> </li> </ul> <ul class="navbar-nav mr-auto w-25"> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> ドロップダウン </a> <ul class="dropdown-menu"> <li><a class="dropdown-item" href="#">Action</a></li> <li><a class="dropdown-item" href="#">Another action</a></li> <li><hr class="dropdown-divider"></li> <li><%= link_to "ログアウト", logout_path, class: "dropdown-item btn btn-light" %></li> </ul> </li> </ul> </div> </div> </nav>
これでログインボタンとログアウトボタンが実装されました。
【23 | Gメール】 << 【ホーム】 >> 【25 | ユーザー管理】
↓↓クリックして頂けると励みになります。