[Rails]ユーザ登録・ログイン 2/20
はじめに
Railsアプリにシンプルなユーザ登録・ログイン機能を作成してみました。
環境
ruby: 3.0.0
rails: 6.1.7
流れ
- Userモデルを作成する
- Userモデルにバリデーションルールをかける
-
routes.rb
に新規登録用urlを追加する - signupコントローラーを作成する
- 新規登録フォームを作成する
- 登録されたユーザをDBに送る
- ログイン機能を作る
- ログアウト機能を作る
- ログインフォームを作る
- Currentモデルを作成する
- エラーメッセージをテンプレート化する
Userモデルを作成する
bin/rails generate model User user_name:string email:string password_digest:string
invoke active_record
create db/migrate/20230615044726_create_users.rb
create app/models/user.rb
bin/rails db:migrate
Running via Spring preloader in process 45777
== 20230615044726 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0021s
== 20230615044726 CreateUsers: migrated (0.0022s) =============================
User.rb
を編集する
class User < ApplicationRecord
has_secure_password
end
User
モデルにhas_secure_password
を追加しています。この1行を追加するだけで、User
モデルはパスワードのハッシュ化、検証、認証に関する機能を利用できるようになります。
モデルにhas_secure_password
を追加すると、パスワードを格納するためのpassword_digest
という名前の属性が自動的に追加されます。また、パスワードの検証や認証に関連するメソッドとして、authenticate
メソッドが提供されます。
has_secure_password
has_secure_password
は、Railsで提供される機能の一つです。この機能を使用すると、ユーザーのパスワードを簡単にハッシュ化して保存し、パスワードの検証や認証に関連する機能を提供することができます。
- パスワードのハッシュ化: パスワードをモデルの属性として扱い、自動的にハッシュ化してデータベースに保存します。ハッシュ化されたパスワードは安全に保存され、元のパスワードは取得できません。
- パスワードの検証: パスワードのバリデーションを自動的に行い、必要に応じてエラーメッセージを追加します。また、パスワードの長さや複雑さの要件を指定することもできます。
- パスワードの認証: ユーザーが入力したパスワードと保存されたハッシュ化されたパスワードを照合し、正しいパスワードかどうかを確認します。認証が成功した場合、対応するユーザーオブジェクトを返します。
bcrypt
をインストールする
has_secure_password
を使うにはbcrypt
というgemが必要です。
Gemfile
の23行目あたりにコメントアウトされていますので、コメントアウトを解除してbundle install
を実行します。
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
bundle install
...
Installing bcrypt 3.1.18 with native extensions
rails console
で確認する
$ rails console
Running via Spring preloader in process 46097
Loading development environment (Rails 6.1.7.3)
irb(main):001:0> User
=> User (call 'User.connection' to establish a connection)
irb(main):002:0> User.all
(1.3ms) SELECT sqlite_version(*)
User Load (0.4ms) SELECT "users".* FROM "users" /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation []> # userがまだいないので空の配列
# testユーザを作る
irb(main):003:0> User.create({user_name:"test_user", email:"user@test.com", password:"password", password_confirmation:"password"})
TRANSACTION (0.1ms) begin transaction
User Create (1.6ms) INSERT INTO "users" ("user_name", "email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["user_name", "test_user"], ["email", "user@test.com"], ["password_digest", "$2a$12$5Cc5BA1GnKH2Dsh/sfhgCuCPgUHyNYqkyEL82xDdyaKtCwjP1lEo."], ["created_at", "2023-06-15 05:26:52.943768"], ["updated_at", "2023-06-15 05:26:52.943768"]]
TRANSACTION (0.8ms) commit transaction
=> #<User id: 1, user_name: "test_user", email: "user@test.com", password_digest: [FILTERED], created_at: "2023-06-15 05:26:52.943768000 +0000", updated_at: "2023-06-15 05:26:52.943768000 +0000">
irb(main):004:0> User.first
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
# passwordとpassword_confirmationがDBに保存されないことを確認する
=> #<User id: 1, user_name: "test_user", email: "user@test.com", password_digest: [FILTERED], created_at: "2023-06-15 05:26:52.943768000 +0000", updated_at: "2023-06-15 05:26:52.943768000 +0000">
Userモデルにバリデーションルールをかける
ユーザ名とメアドにバリデーションをかけて空のままだとユーザを作成できないようにします。
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :user_name, presence: true, length: { minimum: 3 }
end
xxx_create_user.rb
マイグレーションファイルを編集する
バリデーションを通らない場合DBにsaveされないようにします。
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :user_name, null: false
t.string :email, null: false
t.string :password_digest
t.timestamps
end
end
end
# 変更を反映させる
rails db:migrate:redo
rails console
で確認する
メアドを空にしてユーザを作成してみます。
$ rails console
Running via Spring preloader in process 46777
Loading development environment (Rails 6.1.7.3)
irb(main):001:0> user = User.create(user_name:user, password:"password", password_confirmation:"password")
(0.5ms) SELECT sqlite_version(*)
=> #<User id: nil, user_name: nil, email: nil, password_digest: [FILTERED], created_at: nil, updated_at: nil>
irb(main):002:0> user.errors.any?
=> true
irb(main):003:0> user.errors.first
# メアドがblankのエラーが発生
=> #<ActiveModel::Error attribute=email, type=blank, options={}>
routes.rb
に新規登録用urlを追加する
Rails.application.routes.draw do
root to: "main#index"
get "signup", to: "signup#new"
post "signup", to: "signup#create"
end
signupコントローラーを作成する
$ rails g controller signup
Running via Spring preloader in process 47334
create app/controllers/signup_controller.rb
invoke erb
create app/views/signup
class SignupController < ApplicationController
def new
@user = User.new
end
end
新規登録フォームを作成する
app/views/signup
にnew.html.erb
を作成します。
# form_withヘルパーを使って登録フォームを作る
<%= form_with model: @user, url: signup_path do |form| %>
<%= form.text_field :user_name %>
<%= form.text_field :email %>
<%= form.password_field :password %>
<%= form.password_field :password_confirmation %>
<%= form.submit %>
<% end %>
# bootstrapでスタイリングする
<%= form_with model: @user, url: signup_path do |form| %>
<div class="form-group">
<%= form.label :user_name %>
<%= form.text_field :user_name, placeholder: "Please enter your user name (minimum 3 characters)", class: "form-control mb-3" %>
</div>
<div class="form-group">
<%= form.label :email %>
<%= form.text_field :email, placeholder: "user@example.com", class: "form-control mb-3" %>
</div>
<div class="form-group">
<%= form.label :password %>
<%= form.password_field :password, placeholder: "Please enter your password", class: "form-control mb-3" %>
</div>
<div class="form-group">
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation, placeholder: "Please enter your password again", class: "form-control mb-3" %>
</div>
<%= form.submit class:'btn btn-primary' %>
<% end %>
デベロッパーツールで確認します:
フォームを送信してみます。
Started POST "/signup" for ::1 at 2023-06-15 16:46:17 +0900
Processing by SignupController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"user_name"=>"", "email"=>"test@user.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create User"}
Rendering text template
Rendered text template (Duration: 0.1ms | Allocations: 28)
Completed 200 OK in 3ms (Views: 3.0ms | ActiveRecord: 0.0ms | Allocations: 571)
フォームヘルパー:
エラーメッセージを表示させる
バリデーションに引っかかった場合フォームの一番上にエラーメッセージをユーザに表示させます
<% if form.object.errors.any? %>
<div class="alert alert-danger">
<% form.object.errors.full_messages.each do |message| %>
<div>
<%= message %>
</div>
<% end %>
</div>
<% end %>
登録されたユーザをDBに送る
class SignupController < ApplicationController
def new
@user = User.new
end
# actionを追加する
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path, notice: "アカウトを作成しました。"
else
render :new
end
end
def user_params
params.require(:user).permit(:user_name, :email, :password, :password_confirmation)
end
end
ユーザを新規登録してDBにsaveされることを確認します。
Started POST "/signup" for ::1 at 2023-06-15 17:14:08 +0900
Processing by SignupController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"user_name"=>"test_user", "email"=>"user@sample.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create User"}
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/signup_controller.rb:7:in `create'
TRANSACTION (0.0ms) begin transaction
↳ app/controllers/signup_controller.rb:8:in `create'
User Exists? (1.5ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "user@sample.com"], ["LIMIT", 1]]
↳ app/controllers/signup_controller.rb:8:in `create'
User Create (0.6ms) INSERT INTO "users" ("user_name", "email", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["user_name", "test_user"], ["email", "user@sample.com"], ["password_digest", "$2a$12$4I8KZCWM7lfwl80s7w/KxuyqFCBvTlFeaXz49ppv/gXtSI9711rx."], ["created_at", "2023-06-15 08:14:08.342498"], ["updated_at", "2023-06-15 08:14:08.342498"]]
↳ app/controllers/signup_controller.rb:8:in `create'
TRANSACTION (0.8ms) commit transaction
↳ app/controllers/signup_controller.rb:8:in `create'
Redirected to http://localhost:3000/
Completed 302 Found in 217ms (ActiveRecord: 3.6ms | Allocations: 8300)
新規登録機能ができました。ログイン機能も作っていきます。
ログイン機能を作成する
セッションを使ってログインしたユーザを管理します。
class SignupController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
session[:user_id] = @user.id
flash[:success] = "アカウトを作成しました。"
redirect_to root_path
else
render :new
end
end
..
end
ユーザを作成した時に発行されたセッションIDを受け取る
class MainController < ApplicationController
def index
if session[:user_id]
# findの場合はユーザを存在しないとエラーを返すのでfind_byを使う
@user = User.find_by(id: session[:user_id])
end
end
end
DBからユーザを特定してビューに表示させる
DBからuser.id
はsession
IDと等しいユーザを取得します。
Started GET "/" for ::1 at 2023-06-15 20:29:46 +0900
Processing by MainController#index as HTML
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 9], ["LIMIT", 1]]
↳ app/controllers/main_controller.rb:4:in `index'
Rendering layout layouts/application.html.erb
Rendering main/index.html.erb within layouts/application
Rendered main/index.html.erb within layouts/application (Duration: 0.1ms | Allocations: 38)
[Webpacker] Everything's up-to-date. Nothing to do
Rendered shared/_header.html.erb (Duration: 0.2ms | Allocations: 88)
Rendered shared/_message.html.erb (Duration: 0.1ms | Allocations: 50)
Rendered layout layouts/application.html.erb (Duration: 8.3ms | Allocations: 6031)
Completed 200 OK in 11ms (Views: 8.6ms | ActiveRecord: 0.1ms | Allocations: 7016)
<% if @user %>
<h1>こんにちは、<%= @user.user_name %>さん</h1>
<% end %>
ログアウト機能を作成する
ユーザがログアウトしたらセッションIDを削除されます。
routes.rb
を編集する
Rails.application.routes.draw do
...
get "signup", to: "signup#new"
post "signup", to: "signup#create"
delete "logout", to: "sessions#destroy"
end
sessions_controller.rb
を作成する
class SessionsController < ApplicationController
def destroy
session[:user_id] = nil
flash[:success] = "ログアウトしました。"
redirect_to root_path
end
end
ビューにログアウトボタンを追加する
<% if @user %>
<h1>こんにちは、<%= @user.user_name %>さん</h1>
# 良い
<%= button_to "ログアウト", logout_path, method: :delete, class: 'btn btn-primary' %>
# 悪い
<%= link_to "ログアウト", logout_path, method: :delete, class: 'btn btn-primary' %>
<% end %>
button_to
がフォームを使ってpostリクエストを送るようになってます。
ログインフォームを作成する
routes.rb
を編集する
Rails.application.routes.draw do
...
get "signup", to: "signup#new"
post "signup", to: "signup#create"
get "login", to: "sessions#new"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
end
sessions_controller
を編集する
new
とcreate
アクションを追加します。
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user.present? && user.authenticate(params[:password])
session[:user_id] = user.id
flash[:success] = "ログインしました。"
redirect_to root_path
else
flash[:danger] = "ログイン失敗しました。もう一度試してください。"
render :new
end
end
end
ログインフォームを作成する
sessions
コントローラーには特定のモデルが関連付けられていないため、form.errors
メソッドを直接使用することはできません。
SessionsController
や同様のモデルに関連付けられていないコントローラーの場合、エラーメッセージのハンドリング方法が異なります。
form.errors
の代わりに、コントローラーでフラッシュメッセージを設定してエラーを表示する方法を使用します。
<h1>ログイン</h1>
<%= form_with url:login_path do |form| %>
<div class="form-group">
<%= form.label :email %>
<%= form.text_field :email, placeholder: "user@example.com", class: "form-control mb-3" %>
</div>
<div class="form-group">
<%= form.label :password %>
<%= form.password_field :password, placeholder: "Please enter your password", class: "form-control mb-3" %>
</div>
<%= form.submit class:'btn btn-primary' %>
<% end %>
ユーザセッションを全ページに共通するので、application_controller.rb
に入れておきます。
Currentモデルを作成する
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
application_controller.rb
に共通するコードをいれる
main_controller.rb
のindex
アクションを削除します。
class ApplicationController < ActionController::Base
before_action :set_current_user
def set_current_user
if session[:user_id]
Current.user = User.find_by(id: session[:user_id])
end
end
end
@user
をCurrent.user
にする
ビューの<% if Current.user %>
<h1>こんにちは、<%= Current.user.user_name %>さん</h1>
<% end %>
_header.html.erb
を編集する
ユーザがログインしてない場合、ログインと新規登録を表示させます。
ユーザがログインした場合、ユーザ名とログアウトを表示させます
...
<ul class="navbar-nav ms-auto">
<% if Current.user %>
<li class="nav-item">
<%= link_to Current.user.user_name, class: 'nav-link '%>
</li>
<li class="nav-item">
<%= link_to "ログアウト", logout_path, method: :delete, class: 'nav-link' %>
</li>
<% else %>
<li class="nav-item">
<%= link_to "ログイン", login_path, class: 'nav-link' %></li>
<li class="nav-item">
<%= link_to "新規登録", signup_path, class: 'nav-link' %></li>
<% end %>
</ul>
...
エラーメッセージをテンプレート化する
shared/
に_message.html.erb
を作成して、ヘッダーの直下に入れます。
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %> alert-dismissible fade show" role="alert">
<%= value %>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>
終わりに
ユーザ登録・ログイン機能ができました。
次回ではパスワードリセット機能を実装します。
Discussion