Railsチュートリアル 3章 静的なページ
Railsチュートリアル3章をやって大事だと思ったことメモしていきます!
尚、テストコードはRspecを利用して書きました。
静的ページのルーティング、コントローラー
ルーティング
getの4つが静的なページのルーティング。
root_pathは#であるところがちがう。
コントローラー名/アクション名って感じだね。
コントローラー
provideとyeild
application.html.erb
home.html.erb
provideで設定したやつをyeildで呼び出してるイメージ。
そうすることでprovide内で書いたことをyeildで埋め込んでいる。
テストコード
static_pages_spec.rb
system specではgetやpost----pathやrender_template等は使えないと思われる。
visit pathおk
urlもだめ。path◯
3章は以上です!
Railsチュートリアル14章 フォローとフォロワー
14章 フォローとフォロワーの関係がややこしいのでまとめる
ユーザーモデル内
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
active_relationshipsのforeignキーはfollower_idで逆じゃないのか?ってなったがこれで正しい。
これは片方(逆の関係であるもの)を固定することによって片方の情報を持ってきている。
図を見た方がわかりやすい(14.7)
active
followed | folower
1 2
2 1
3 1
4 1
foreignキーでfollewer_idを指定する(今回であれば1)によってフォローしているfollowedの情報を取ってくる。
has_many :followers, through: :passive_relationships, source: :follower
上と考え方一緒。逆の関係
図(14.9)
.
.
.
end
Rails チュートリアル13章
今回は13章をやっていく中で必要だと思ったところをメモしていきます。
・複合キーインデックス
micropostのマイグレーションファイル
class CreateMicroposts < ActiveRecord::Migration[6.0] def change create_table :microposts do |t| t.text :content t.references :user, foreign_key: true t.timestamps end add_index :microposts, [:user_id, :created_at] end end
class Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 } end
<li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <span class="user"><%= link_to micropost.user.name, micropost.user %></span> <span class="content"><%= micropost.content %></span> <span class="timestamp"> Posted <%= time_ago_in_words(micropost.created_at) %> ago. </span> </li>
class StaticPagesController < ApplicationController def home @micropost = current_user.microposts.build if logged_in?
buildになっている end def help end def about end def contact end end
<% if logged_in? %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> </div> <% else %> <div class="center jumbotron"> <h1>Welcome to the Sample App</h1> <h2> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %> <% end %>
<%= link_to gravatar_for(current_user, size: 50), current_user %> <h1><%= current_user.name %></h1> <span><%= link_to "view my profile", current_user %></span> <span><%= pluralize(current_user.microposts.count, "micropost") %></span>
<%= form_with(model: @micropost, local: true) do |f| %> <%= render 'shared/error_messages', object: f.object %> <div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> </div> <%= f.submit "Post", class: "btn btn-primary" %> <% end %>
class User < ApplicationRecord . . . # 試作feedの定義 # 完全な実装は次章の「ユーザーをフォローする」を参照 def feed Micropost.where("user_id = ?", id) end private . . . end
class StaticPagesController < ApplicationController def home if logged_in? @micropost = current_user.microposts.buildapp/views/static_pages/home.html.erb
このmicropostは先ほど書いたやつ(build)なので関係ない @feed_items = current_user.feed.paginate(page: params[:page]) end end def help end def about end def contact end end
<% if logged_in? %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> </div> <% else %> . . . <% end %>
app/views/shared/_feed.html.erb
<% if @feed_items.any? %> <ol class="microposts"> <%= render @feed_items %> </ol> <%= will_paginate @feed_items %> <% end %>
<%= render @feed_items %>
@feed_itemsはMicropostsのクラスを持っているため、micropostsのパーシャルを呼び出すことができた。
app/views/microposts/_micropost.html.erbを呼び出してる
microposts作成失敗の場合に備えて
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save flash[:success] = "Micropost created!" redirect_to root_url else @feed_items = current_user.feed.paginate(page: params[:page]) render 'static_pages/home' end end def destroy end private def micropost_params params.require(:micropost).permit(:content) end end
ページネートでエラーが出るので解決。
micropostsをcreateするcreateアクションがMicropostsコントローラーにあるので、Micropostsコントローラーでページネートしようとしている。
そこで、コントローラーとアクションを指定してあげることで解決する。
app/views/shared/_feed.html.erb
<% if @feed_items.any? %> <ol class="microposts"> <%= render @feed_items %> </ol> <%= will_paginate @feed_items, params: { controller: :static_pages, action: :home } %> <% end %>
Railsチュートリアル12章 パスワード再設定
Password Reset
ログインする際、パスワードを忘れてしまった場合にパスワード再設定をする。
userのedit,updateでパスワードを変えるのは違う。それはパスワードが分かっていててログインできてる状態。
今回はパスワードが分からなくて、ログインできない状態
全体像
ログインページからpassword_fogetをクリックしてパスワード再設定ページへ。
emailを入力すれば(new),createアクションでreset_token,reset_digestを生成。
reset_digestをカラムに入れる。またこのcreate アクションでメールを送る。
メール内に書かれたedit_pathをクリック。edit_pathでpassword とpassword_confirmationを入力(edit)するとupdateアクションへ。passwordとpassword_confirmationをupdateしてあげる。
変更されたpasswordでログインできるようになる。
-----------------------------------------------------------------------------------------------------------------
password_resetコントローラー作成
$ rails generate controller PasswordResets new edit --no-test-framework
ルーティング追加
config/routes.rb
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] end
ログインページにパスワード再設定ページへのリンクを追加
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= link_to "(forgot password)", new_password_reset_path %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>
$ rails generate migration add_reset_to_users reset_digest:string \ > reset_sent_at:datetime
$ rails db:migrate
パスワード再設定ページ
app/views/password_resets/new.html.erb
<% provide(:title, "Forgot password") %> <h1>Forgot password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: password_resets_path, scope: :password_reset, local: true) do |f| %>
urlとscopeを書けば、モデルでなくても大丈夫 <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.submit "Submit", class: "btn btn-primary" %> <% end %> </div> </div>
コントローラー createアクション
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end end
app/models/user.rb
class User < ApplicationRecord attr_accessor :remember_token, :activation_token, :reset_token before_save :downcase_email before_create :create_activation_digest . . . # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end # パスワード再設定の属性を設定する def create_reset_digest self.reset_token = User.new_token update_attribute(:reset_digest, User.digest(reset_token)) update_attribute(:reset_sent_at, Time.zone.now) end # パスワード再設定のメールを送信する def send_password_reset_email UserMailer.password_reset(self).deliver_now end private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end
メール
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset(user) @user = user mail to: user.email, subject: "Password reset" end end
app/views/user_mailer/password_reset.text.erb
To reset your password click the link below: <%= edit_password_reset_url(@user.reset_token, email: @user.email) %> This link will expire in two hours. If you did not request your password to be reset, please ignore this email and your password will stay as it is.
app/views/user_mailer/password_reset.html.erb
<h1>Password reset</h1> <p>To reset your password click the link below:</p> <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %> <p>This link will expire in two hours.</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p>
プレビュー
test/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first
user.activation_token = User.new_token
UserMailer.account_activation(user)
end
# Preview this email at
# http://localhost:3000/rails/mailers/user_mailer/password_reset
def password_reset
user = User.first
user.reset_token = User.new_token
UserMailer.password_reset(user)
end
end
サーバーログにメールが送られて来ている。
editのビュー
app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %>
updateアクションでもemailからreset_tokenを持ってくる必要があるため。
f.hidden_fieldにしてしまうとparams[:user][:email]になってしまう。
hidden_fieldだとparam[:email] <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div>
class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] . . . def edit end private def get_user @user = User.find_by(email: params[:email]) end # 正しいユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end end
コントローラー update
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] before_action :check_expiration, only: [:edit, :update] # (1)への対応 def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end def update if params[:user][:password].empty? # (3)への対応 @user.errors.add(:password, :blank) render 'edit' elsif @user.update(user_params) # (4)への対応 log_in @user flash[:success] = "Password has been reset." redirect_to @user else render 'edit' # (2)への対応 end end private def user_params params.require(:user).permit(:password, :password_confirmation) end # beforeフィルタ def get_user @user = User.find_by(email: params[:email]) end # 有効なユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end # トークンが期限切れかどうか確認する def check_expiration if @user.password_reset_expired? flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end end
app/models/user.rb
class User < ApplicationRecord . . . # パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago end
2時間前より早い=2時間を過ぎていると解釈する private . . . end
app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController . . . def update if params[:user][:password].empty? @user.errors.add(:password, :blank) render 'edit' elsif @user.update(user_params) log_in @user @user.update_attribute(:reset_digest, nil)
2時間の間に不正アクセスされないようにするため。 flash[:success] = "Password has been reset." redirect_to @user else render 'edit' end end . . . end
Railsチュートリアル11章 アカウントの有効化
アカウントの有効化
ユーザーを新規登録する際にメールを送って、メール内に貼ってあるリンクをクリックすることで、初めてログインできるようになる。
全体の流れ
ユーザーがsave出来たら、before_createでactivation_tokenを作り、それをdigest化してハッシュにしたactivation_digestを作る(self.activation_digestとなっているのでuserのカラム内に入る)。そしてuserコントローラー内でメールを送る。この段階でuserはcreateされている。このメール内のedit_pathに行くとauthenticatedで、先ほどdigest化したやつとactivation_tokenが合っているか確かめる。合っていたらactivationコントローラー内のeditアクションでactivatedをtrueにする。セッションコントローラーでactivatedがtrueであればログインできる。falseであればログインできないように記述する。これによって初めてログインできるようになる。
--------------------------------------------------------------------------------------------------------------------
まずはacctount_activationコントローラー作る
$ rails generate controller AccountActivations
ルーティング
edit設定
config/routes.rb
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] end
マイグレーション
activation_digestカラム等作る
$ rails generate migration add_activation_to_users \ > activation_digest:string activated:boolean activated_at:datetime
class AddActivationToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end end
$ rails db:migrate
モデル内にactivation_token,activation_digestを作る記述をかく。
app/models/user.rb
*before_saveのところは追加しているのではなく変更しているだけ。
class User < ApplicationRecord attr_accessor :remember_token, :activation_token before_save :downcase_email before_create :create_activation_digest validates :name, presence: true, length: { maximum: 50 } . . . private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end
メールを送信するメイラーを作る
$ rails generate mailer UserMailer account_activation password_reset
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base default from: "noreply@example.com" layout 'mailer' end
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset @greeting = "Hi" mail to: "to@example.org" end end
app/views/user_mailer/account_activation.text.erb
Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
app/views/user_mailer/account_activation.html.erb
<h1>Sample App</h1>
<p>Hi <%= @user.name %>,</p>
<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>
<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
email: @user.email) %>
emailの情報からuserのactivation_tokenを持ってきている。
activation_digestはカラムに入れたが、activation_tokenはカラムに入れてないから@userで持ってこれないからだと思う。
カラムに入れない理由はdbで情報を盗まれたら困るため。そのためハッシュ化したactivation_digestをカラムに入れてる。
送信メールのプレビュー
メールを送らずともメールの画面を確認できる。
config/environments/development.rb
Rails.application.configure do
.
.
.
config.action_mailer.raise_delivery_errors = false
host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
# クラウドIDEの場合は以下をお使いください
config.action_mailer.default_url_options = { host: host, protocol: 'https' }
# localhostで開発している場合は以下をお使いください
# config.action_mailer.default_url_options = { host: host, protocol: 'http' }
.
.
.
end
localhostの場合は
host = 'localhost:3000' # ローカル環境
config.action_mailer.default_url_options = { host: host, protocol: 'http' }
test/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
# Preview this email at #
http://localhost:3000/rails/mailers/user_mailer/account_activation
def account_activation
user = User.first user.activation_token = User.new_token UserMailer.account_activation(user)
end #
Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end
test/mailers/previews/user_mailer_preview.rb
ユーザーのcreateアクションにメール送信記述を書く。
app/controllers/users_controller.rb
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params)
if @user.save
UserMailer.account_activation(@user).deliver_now
flash[:info] = "Please check your email to activate your account."
redirect_to root_url
else
render 'new'
end
end
.
.
.
end
メールはローカル環境であればサーバーログ内(ターミナル内)で確認。
urlが書いてあるのでそれを持ってきて貼るとメールが確認できる。
authenticated?メソッド
app/models/user.rb
class User < ApplicationRecord
.
.
.
# トークンがダイジェストと一致したらtrueを返す
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
.
.
.
end
この形にしているのは、acctivation_digestの他に、クッキーのremember_digestでも使うため。
どちらでも対応できるようにするためsendメソッドを用いて、引数で送られてきたやつのdigestを取り扱っている。
app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
# 現在ログイン中のユーザーを返す(いる場合)
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
.
.
.
end
引数を2つに変更する。
app/controllers/account_activations_controller.rb
メール内のedit_pathをクリックしたときのアクション
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
activatedがtrueであれば、ログインできるようにsessionコントローラーに記述する
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
リファクタリング
private
.
.
.
end
class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save @user.send_activation_email flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end
app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
有効なユーザーだけを表示するコードのテンプレート
app/controllers/users_controller.rb
class UsersController < ApplicationController . . . def index @users = User.where(activated: true).paginate(page: params[:page]) end def show @user = User.find(params[:id]) redirect_to root_url and return unless activated? end . . . end
本番環境でのメール送信
mailgunを使用する
config/environments/production.rb
Rails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp host = '<あなたのHerokuサブドメイン名>.herokuapp.com' config.action_mailer.default_url_options = { host: host } ActionMailer::Base.smtp_settings = { :port => ENV['MAILGUN_SMTP_PORT'], :address => ENV['MAILGUN_SMTP_SERVER'], :user_name => ENV['MAILGUN_SMTP_LOGIN'], :password => ENV['MAILGUN_SMTP_PASSWORD'], :domain => host, :authentication => :plain, } . . . end
herokuのurlでok
$ rails test $ git add -A $ git commit -m "Add account activation" $ git checkout master $ git merge account-activation
$ rails test $ git push $ git push heroku $ heroku run rails db:migrate
$ heroku addons:create mailgun:starter
MailgunのHerokuアドオンを追加する
$ heroku config:get MAILGUN_SMTP_LOGIN $ heroku config:get MAILGUN_SMTP_PASSWORD
今回の場合は環境変数が自動で設定されるみたいなのでこれら必要ないみたい。
Railsチュートリアル9章 クッキー(remember me)
クッキー
第9章で学んだクッキーについてまとめる。
テストコードは省略する。
まずはmigration追加
$ rails generate migration add_remember_digest_to_users remember_digest:string
class AddRememberDigestToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :remember_digest, :string end end
$ rails db:migrate
app/models/user.rb
class User < ApplicationRecord before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true has_secure_password validates :password, presence: true, length: { minimum: 6 } # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end end
# これでランダムな文字列の組み合わせとしてトークンを作っている
# 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end
# 作成したremember_tokenをdigest化でハッシュにしてそれをremember_digestとして保存している。
# 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) end
# ハッシュ化したremember_digest(dbのカラムに保存されてる)と引数として渡されたremember_tokenが合致するか確かめる。
BCryptを使うことでハッシュ後とハッシュ前でも比較できる。
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user remember user
ヘルパー内の記述に飛ぶ redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out redirect_to root_url end end
app/helpers/sessions_helper.rb
module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # ユーザーのセッションを永続的にする def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end
#ここでクッキーに情報を入れている。 # 現在ログインしているユーザーを返す(いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end end
current_user変更
module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # ユーザーのセッションを永続的にする def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end # 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end
user_id = session[:user_id]はイコールではなく、代入。user_idはただの変数。
だから最初のifはsession[:user_id]があればでok,あったらuser_idに代入する。
なかったらelsif文にいく。
ここでauthencticated?を使ってクッキーにあるremember_tokenとdbのカラムに保存したrember_digestを比較してあげてる。
# ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end end
ユーザーを忘れる
app/models/user.rb
class User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true has_secure_password validates :password, presence: true, length: { minimum: 6 } # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end # 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end # 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) end # ユーザーのログイン情報を破棄する def forget update_attribute(:remember_digest, nil) end end
app/helpers/sessions_helper.rb
module SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end . . . # 永続的セッションを破棄する def forget(user) user.forget cookies.delete(:user_id) cookies.delete(:remember_token) end # 現在のユーザーをログアウトする def log_out forget(current_user) session.delete(:user_id) @current_user = nil end end
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController . . . def destroy log_out if logged_in? redirect_to root_url end end
app/models/user.rb
これはテストのせいで追加
class User < ApplicationRecord . . . # 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end # ユーザーのログイン情報を破棄する def forget update_attribute(:remember_digest, nil) end end
[remember me]チェックボックス
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user)
チェックボックスにチェックを入れると1でremember,チェックを入れないと0でfoeget redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end
Railsチュートリアル8章 セッション
セッション
セッションについてまとめる。テストコードは省略する。
コントローラー作成
rails generate controller Sessions new
ここでnewを使っているのはコントローラーと同時にnewのビューを作りたいため。
他のアクションはビュー必要ないので後で定義
ルーティング
get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy'
名前付きのルートを作っているが、resourcesでも問題はない
ビュー
app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_with(url: login_path, scope: :session, local: true) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
コントローラー
app/controllers/sessions_controller.rb
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
#authenticateは入力したパスワードがハッシュ化されてdbにあるパスワードと合致するかをしている。
authenticateはモデル内にhas_secure_passwordを記述したことで使えるようになる。
has_secure_passwordを使うにはbycriptというgemが必要。
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
ログインメソッド
ログインメソッドをヘルパー内に書く。
そしてそれをコントローラー内でも使えるようにapplicationコントローラーに入れ込む。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include SessionsHelper
end
app/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
コントローラーに追加
loginメソッドを入れる
app/controllers/sessions_controller.rb
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
現在のuser
app/helpers/sessions_helper.rb
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
ログインしているかどうか(同じくhelper内)
def logged_in?
!current_user.nil?
end
ビュー
app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
ドロップダウンを作るためjavascriptを入れる。
config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
)
module.exports = environment
app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("jquery")
import "bootstrap"
Userモデル内
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
現段階ではテストコードにしか使わないのでスルーでおk
ユーザー登録時にログインできるようにする
app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
ログアウト
app/helpers/sessions_helper.rb
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
.
.
.
# 現在のユーザーをログアウトする
def log_out
session.delete(:user_id)
@current_user = nil
end
end
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end