はじめに
Rails6.1ハンズオン(2)の続きです。
環境は前回を参考にしてください。
いつRails6.1固有の機能にたどり着くのか...
今回はメールアドレスとパスワードを使った、
ユーザーログイン・ログアウトの機能を追加します。
コードはGithubにあげています。章ごとにコミットしてますので、参考にしていただければ幸いです。
やること
Railsでユーザー関連の機能をつけるときは、Railsチュートリアルのように自前で全部用意することも可能ですが、今回は楽をするためにGemを使います。
定番は「devise」でしょうか?仕事でもよく使いますが、せっかくなので、「sorcery」を使ってみようと思います。
最近(21/2/17くらい)v0.16.0になり、Rails6に対応したようです。ぴったりですね!
実装
参考にさせていただくもの:
Sorcery/sorcery
docker-compose下でrails newして Rails6.1 + Sorcery を試す( Sorcery の仕組み少し解説)
シンプル認証gem sorceryを完全入門するで!! - Qiita
4-1. sorcery gem導入、Userクラスをつくる
Gemfileにて以下を追加:
gem 'sorcery'
gem 'bcrypt'
↓これに準拠して進めます
Sorcery/sorcery
コマンドを実行:
bundle install
bundle exec rails g sorcery:install
File unchanged! The supplied flag value not found! app/models/user.rb
と赤字で出た。なんだろう... コマンドオプションを設定するとでないのかな?
とりあえずヨシ!
gコマンドで生成されたmigrationファイルでテーブル名がUserになっているのをusersに直す。(Userのまま進めたらエラーになりました)
db/migrate/(数字)_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :crypted_password
t.string :salt
t.timestamps null: false
end
add_index :users, :email, unique: true
end
end
Userモデルのバリデーション設定を追加する。
app/models/user.rb
class User < ApplicationRecord
authenticates_with_sorcery!
validates :email, uniqueness: true, presence: true
validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
end
チュートリアルではemailのバリデーションにpresence: true
が入っていなかったので追加。
4-2. ユーザー作成・編集機能
rails g controller Users new create edit update
app/views/layouts/_header.html.haml(セッション機能は後で作る)
%nav.navbar.navbar-expand-lg.navbar-dark.bg-dark
.container-fluid
= link_to root_path, class: 'navbar-brand' do
Rails6.1掲示板
%button.navbar-toggler{"aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", :type => "button"}
%span.navbar-toggler-icon
#navbarSupportedContent.collapse.navbar-collapse
%ul.navbar-nav.me-auto.mb-2.mb-lg-0
%li.nav-item
= link_to root_path, class: 'nav-link' do
トップ
%li.nav-item
= link_to new_community_path, class: 'nav-link' do
コミュニティを作る
.d-flex
-if current_user
.nav-item.dropdown
%a.nav-link.text-white.dropdown-toggle#navbarDropdownMenuLink{href: "#", role: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false"}
= current_user.email
%ul.dropdown-menu{"aria-labelledby": "navbarDropdownMenuLink"}
%li
= link_to t('header.to_user_edit'), edit_user_path(current_user.id), class: 'dropdown-item'
%li
= link_to t('header.to_logout'), :logout, method: :post, class: 'dropdown-item'
- else
= link_to t('header.to_register'), new_user_path, class: 'btn btn-primary me-2'
= link_to t('header.to_login'), :login, class: 'btn btn-secondary'
app/controllers/users_controller.rb(require_loginメソッドは後で作ります)
class UsersController < ApplicationController
skip_before_action :require_login, only: %i[new create]
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path, notice: 'ユーザーを新規作成しました。'
else
render :new
end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to root_path, notice: 'ユーザー情報を変更しました。'
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
app/views/users/new.html.haml
%h1= t '.title'
= form_with model: @user do |f|
= render 'shared/error_messages', object: f.object
= f.label :email, class: 'form-label'
= f.text_field :email, class: 'form-control mb-2'
= render 'form', form: f
= f.submit t('.submit'), class: 'btn btn-primary'
app/views/users/edit.html.haml
%h1= t '.title'
= form_with model: @user do |f|
= render 'shared/error_messages', object: f.object
= render 'form', form: f
= f.submit t('.submit'), class: 'btn btn-primary'
app/views/users/_form.html.haml
= form.label :password, class: 'form-label'
= form.password_field :password, class: 'form-control mb-2'
= form.label :password_confirmation, class: 'form-label'
= form.password_field :password_confirmation, class: 'form-control mb-2'
app/views/shared/_error_messages.html.haml
- if object.errors.present?
.card.mb-2.border-danger
.card-header.bg-danger.text-white
エラーがあります
.card-body
%ul
- object.errors.each do |e|
%li= e.full_message
config/locales/model.ja.yml
config/locales/views/users/ja.yml
config/routes.rb
をいじる(コードは省略、リポジトリをみてください)
4-3. セッション機能
rails g controller UserSessions new create destroy
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :require_login
private
def not_authenticated
redirect_to login_path, alert: 'ログインしてください。'
end
end
require_loginはsorceryにもともと入っているメソッドらしい。
ログインしていなかったらnot_authenticatedメソッドを呼ぶという挙動。
not_authenticatedも忘れずに定義しておく。
app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
skip_before_action :require_login, only: %i[new create]
def new; end
def create
@user = login(params[:email], params[:password])
if @user
redirect_back_or_to(root_path, notice: 'ログインしました。')
else
flash.now[:alert] = 'ログインできませんでした。'
render :new
end
end
def destroy
logout
redirect_to(login_path, notice: 'ログアウトしました。')
end
end
login(メアド, パスワード)でログインできる。
redirect_back_or_toで#not_authenticatedでリダイレクトする前のページに戻せる。
logout()でログアウト。
app/views/user_sessions/new.html.haml
%h1= t '.title'
= form_with url: login_path, method: :post do |f|
= f.label t('.email'), class: 'form-label'
= f.text_field :email, class: 'form-control mb-2'
= f.label t('.password'), class: 'form-label'
= f.password_field :password, class: 'form-control mb-4'
= f.submit t('.submit'), class: 'btn btn-primary'
config/routes.rb
Rails.application.routes.draw do
root to: 'communities#index'
get 'login' => 'user_sessions#new', as: :login
post 'login' => 'user_sessions#create'
post 'logout' => 'user_sessions#destroy', as: :logout
resources :users
resources :communities, only: %i[index new create show] do
resources :comments, only: %i[new create]
end
end
4-4. おまけ
flashメッセージを表示する。
app/views/layouts/application.html.haml
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title Rails61HandsOn
%meta{:content => "width=device-width,initial-scale=1", :name => "viewport"}/
= csrf_meta_tags
= csp_meta_tag
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
%body
= render '/layouts/header'
- if flash[:notice]
.alert.alert-info.m-2= flash[:notice]
- if flash[:alert]
.alert.alert-warning.m-2= flash[:alert]
.container-fluid.pt-4.px-4
= yield