Rails Tutorial 6th Edition Learning Summary Chapter 8

Overview

This article deepens my understanding by writing a Rails tutorial commentary article to further solidify my knowledge It is part of my study. In rare cases, it may contain ridiculous or incorrect content. Please note. I would appreciate it if you could tell me implicitly ...

Source Rails Tutorial Chapter 6

What to do in this chapter

Since we made it possible to create a user in the previous chapter, we will create a mechanism to log in with the created user.

session

Since HTTP is stateless (there is no state), it is not possible to use previous request information. There is no way to retain data in HTTP when you move to another page. Instead, a "session" is used to retain user information.

Cookies are often used to implement this session in Rails Cookies are text data stored in the browser and do not disappear even if you move the page, so you can retain user information.

Sessions controller

Session is also implemented by directly linking it to REST actions. In particular

motion action
Login form new
Login process create
Logout process destroy

Login → Create temporary session Log out → Delete temporary session

First, create a controller for the session. rails g controller sessions new

Creating a controller with the generate command also creates a corresponding view. This time the view is only needed for the new action on the login form Specify only new with the generate command.

Also update routes.rb. Although routing to the new action is defined rewrite.

  get '/login', to:'sessions#new'
  post '/login', to:'sessions#create'
  delete '/logout', to:'sessions#destroy'

Change the auto-generated test to use the named route as well.

rails routesYou can check the list of routes by using the command.

Exercise

  1. get login_path receives a login form with a GET request. post login_pathSends the value entered in the login form in a post request.

$ rails routes | grep sessions
                                login GET    /login(.:format)                                                                         sessions#new
                                      POST   /login(.:format)                                                                         sessions#create
                               logout DELETE /logout(.:format)                                                                        sessions#destroy

Login form

In the form, we have implemented the form using form_with. The login form does not create a Users resource, it just creates a Session Model object cannot be specified. In such a case

<%= form_with(url: login_path, scope: :session, local: true) do |f| %>


 Send a Post request to login_path by
 By setting scope:: session
 The values of various attributes will be stored in params [: session].
 For example, password
 Stored in params [: session] [: password].

##### Exercise
1.
 There are two types of login_path, but form_with is set to POST request by default.
 Issue a POST request to login_path. In other words, the create action is reached with the POST login_path.
 In the case of GET, the new action is reached.

#### User search and authentication
 This time, we will create a process when login fails.

```rb
 def new
  end
  
  def create
     render 'new'
  end
  
  def destroy
  end

If you define the controller like this When you submit the form, it will be forwarded to the new page as it is.

If you try to submit the form in this state image.png

You can see that the session key has been added to the params hash, which contains the email and password keys.

params = { session: { password: "foobar", email: "[email protected]" }}

It has such a structure.

With this Receive user credentials from params and search for the user with the `` `find_bymethodauthenticate```After authenticating the password with the method Process in the flow of creating a session.

Click here for the contents of the create action based on this

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
    #Process to redirect to user page(from now on)
    else
   #Create an error message.
      render 'new'
    end
  end

&& is an operator of logical product and is true only when both are true. The condition is that the user exists and the user's password is authenticated.

Exercise
  1. The expected result is obtained.
>> user = nil
=> nil
>> user && user.authenticate("foobar")
=> nil
>> !!(user && user.authenticate("foobar"))                                                                                
=> false
>> user = User.first
   (1.4ms)  SELECT sqlite_version(*)
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-14 02:57:10", password_digest: [FILTERED]>
>> !!(user && user.authenticate("foobar"))
=> true

Show flash message

Create a flash message when login fails.

danger] = "Invalid email/password combination"



 If this is left as it is, the message will remain even when the page is changed to another page.
 Because I'm redisplaying the form in render.
 Since render is a view re-render, not a redirect, flash doesn't disappear.
 I will fix this problem from now on.

 ↓ The flash is displayed on other pages ↓
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617753/4bbfd1e0-1e51-1de0-0b09-44394f9d9723.png)


#### Flash test
 Create an integration test around login. This time there is a problem that the flash is displayed on other pages as well, so this error
 Create the test to detect first and write the code so that the test passes.

 Click here for this code

```rb
  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path , params:{ session: { email:"",password:"" }}
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end

The last two lines access the root path and test that the flash is off.

Of course, an error will occur. Use flash.now to solve this problem.

flash.now disappears when you move to the next action.


 ↑ Reference site (https://qiita.com/taraontara/items/2db82e6a0b528b06b949)

 I was confused for a moment that everything should be flash.now, but in the case of flash.now, when I use redirect_to, flash is never displayed in the first place
 It's over. (To go read the next action with redirect_to)

 Immediately change ``` flash``` to `` `flash.now```.
 This change will pass the test.

##### Exercise
1.
 Login failure
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617753/341a1295-c6d4-e19b-1947-88e9044042f4.png)
 It disappears when you move to another page
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617753/09cc611d-ce89-3f2c-0015-a7018fba1f01.png)


### Login
 Next, implement the process when the login information is valid.
 The processing flow is
 Authenticate, create a temporary session using cookies if the user is correct, complete the login process,
 Transit to the corresponding user page.

 Sessions are processed separately depending on whether you are logged in or not, even on pages other than the login page.
 Include SessionsHelper in ApplicationController so that it can be used in common.
 By doing this, it can be used in common by all controllers.

#### log_in method
 Implement login process using ``` session``` method defined in Rails.
 When the session method is used, the value associated with the key is saved in temporary cookies.
 These temporary cookies expire the moment the browser is closed, so you need to log in each time you close it.

 Since login is expected to be used in various places, define it as a method in SessionsHelper.


#### **`sessions_helper.rb`**
```rb

module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end 
end

After that, add it to the create action and redirect it to complete it!

  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
Exercise
  1. In the case of Chrome, you can display the list with Cookies on the Application tab of the developer tools. image.png ↑ The encrypted user ID is stored (_sample_app_session)

  2. Expires → The time to expire is written. In the case of a temporary session, it is Session, and you can see that it expires when the browser is closed.

Current user

Define the `` `current_user``` method so that the user ID can be retrieved and the user information can be used on another page.

Searching for a user object from the ID stored in the session, but using find will cause an error if the user does not exist. Use the find_by method.

  def current_user
    if session[:user_id]
      User.find_by(id: session[:user_id])
    end
  end

Furthermore, by saving this result (User object) in an instance variable Database reference within one request can be done only once, which leads to speedup.

  def current_user
    if session[:user_id]
      if @current_user.nil?
        @current_user = User.find_by(id: session[:user_id])
      else
        @current_user
      end
    end
  end

And||You can write this if statement in one line by using an operator.

@current_user = @current_user || User.find_by(id: session[:user_id])



 In addition, writing this line in abbreviated form will give the correct code.

#### **`@current_user ||= User.find_by(id: session[:user_id])`**
Exercise
>> User.find_by(id:"123")
   (0.4ms)  SELECT sqlite_version(*)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 123], ["LIMIT", 1]]
=> nil

(1) If nil is given, the find_by method returns nil and @current_user also becomes nil. ② When @ current_user is empty, read from the database, but when there is content, substitute confidence (do nothing)

>> session = {}
=> {}
>> session[:user_id] = nil
=> nil
>> @current_user ||= User.find_by(id:session[:user_id])
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ?  [["LIMIT", 1]]
=> nil
>> session[:user_id] = User.first.id
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 1
>> @current_user ||= User.find_by(id: session[:user_id])
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-14 02:57:10", password_digest: [FILTERED]>
>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-14 02:57:10", password_digest: [FILTERED]>
>> 

Change the layout link

Dynamically change the displayed page depending on whether you are logged in or not. If you branch in ERB depending on whether you are logged in or not, change the link. Define a `` `logged_in? ``` method that returns a logical value to this.

  def logged_in?
    !current_user.nil?
  end

I want to make sure that current_user is not empty, so I'm reversing the logical value with!

Next, change the header layout.

erb:_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>

I'm adding a dropdown menu using a bootstrap class (such as dropdown). Also <%= link_to "profile", current_user %>In the line The link is specified in abbreviated form The link will be to the show page. It is a function that converts a user object into a link to the show page.

The dropdown menu has to load jQuery in bootstrap, so Load through application.js.

First, load the jQuery and bootstrap packages with yarn.

yarn add [email protected] [email protected]



 Next, add the jQuery settings to the Webpack environment file.


#### **`config/webpack/environment.js`**
```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

Finally, require jQuery in application.js and import Bootstrap.

At this point the drop-down menu is enabled and Since you can log in as a valid user You can efficiently test your code so far.

Also, when you close the browser, the temporary session is deleted as expected and you need to log in again.

Exercise
  1. Chrome cookie deletion screen After deleting, the login link is displayed. image.png

  2. Omitted. You will be prompted to log in when you restart your browser. (Temporary session disappears.)

Test layout changes

We will test the behavior of dynamically changing header links with integration tests. The operation is

  1. Access your login path
  2. Post login information
  3. Test that the login link is hidden
  4. Test that the logout link is displayed
  5. Test that the profile link is displayed

You must first log in as a registered user to test these The user must be registered in the test database. In Rails, you can create test data by using fixture.

Since the user data is registered but the password is hashed and saved as password_digest The password that hashed the fixture data also needs to be stored in password_digest

Defines a digest method that returns a hashed string of password Since it is used for fixtures, it is not used for user objects. Therefore, it is defined as a class method.

Also, to reduce weight, the cost of hashing should be minimized during testing, and the cost should be increased for security in a production environment. Click here for the defined method

  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine::cost
    BCrypt::Password.create(string, cost: cost)
  end

Now that the digest method is ready, create a fixture.

users.yml


michael:
  name: Michael Example
  email: [email protected] 
  password_digest: <%= User.digest('password') %>

Since erb can be used in the fixture, the character string hashed with "password" is assigned to password_digest using the digest method. You can now refer to the user from the fixture.

I will create a test at once.

  test "login with valid information" do
    get login_path
    post login_path , params:{session:{email: @user.email,
                                       password: 'password'}}
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end

In this test assert_redirected_toThe redirect destination is@userI'm checking if it is. follow_redirect!Actually move to the redirect destination with. After that, is the show page (view) displayed with assert_template? assert_selectThere is no login link in. There is a logout link. Testing for show page links.

Exercise
  1. Modify the post line like this.

post login_path , params:{ session: { email: @user.email,password:"" }}


 Now you can test the case where your email address is valid and your password is invalid.

2.```user && user.authenticate(params[:session][:password])```
↓

#### **`user&.authenticate(params[:session][:password])`**

These two are equivalent

Login at the time of user registration

If you do not log in after registering as a user, it will be confusing and cause confusion for users, so log in at the same time as you register. Since the login process is defined by the log_in method, add the `` `log_in``` method to the Create action of UsersController Just add it.

  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

To determine if you are logged in when you create the user you added here I want to use the `logged_in? ``` method added to the helper method As a helper method for testing cannot be called in the test, test_helper.rb``` Register new. This is defined as the ```is_logged_in? `` Method to prevent confusing names.

  def is_logged_in?
    !session[:user_id].nil?
  end
Exercise
  1. It becomes RED because I am going to check if I am logged in with the is_logged_in? Method.
  2. Comment out can be done by selecting the target line and pressing the Windows key + /.

Log out

We will implement the logout function. Since there is a link, we will define the action. The content of the process is the reverse of the log_in method and the session can be deleted.

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end

Also create a destroy action using the defined `` `log_out``` method.

  def destroy
    log_out
    redirect_to root_url
  end

Specifications to log out and jump to the root URL when accessed.

Update the test here as well.

user_login_test.rb


require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
  end
  
  test "login with valid email/invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path , params:{ session: { email: @user.email,password:"" }}
    assert_not is_logged_in?
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
  
  test "login with valid information" do
    get login_path
    post login_path , params:{session:{email: @user.email,
                                       password: 'password'}}
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path ,count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

Here, whether the logout process is being performed, whether the redirect is successful, whether the displayed link is correct, etc. I'm testing.

Also, the contents of "login with valid email / invalid password" are rewritten by using the ```is_logged_in? `` `Method.

Exercise

  1. The session is deleted and jumps to the root URL. The links on the page will also change to those before login. We have confirmed the operation, so it is omitted.

  2. It doesn't disappear when using Chrome, but the logout process is done. debuggerYou can also check it directly using such as.

[12, 21] in /home/ubuntu/environment/sample_app/app/controllers/sessions_controller.rb
   12:       render 'new'
   13:     end
   14:   end
   15:   
   16:   def destroy
   17:     log_out
   18:     redirect_to root_url
   19:     debugger
=> 20:   end
   21: end
(byebug) logged_in?
false

To the previous chapter

To the next chapter

Recommended Posts

Rails Tutorial 6th Edition Learning Summary Chapter 10
Rails Tutorial 6th Edition Learning Summary Chapter 7
Rails Tutorial 6th Edition Learning Summary Chapter 4
Rails Tutorial 6th Edition Learning Summary Chapter 6
Rails Tutorial 6th Edition Learning Summary Chapter 5
Rails Tutorial 6th Edition Learning Summary Chapter 2
Rails Tutorial 6th Edition Learning Summary Chapter 3
Rails Tutorial 6th Edition Learning Summary Chapter 8
Rails Tutorial (4th Edition) Summary
Rails Tutorial (4th Edition) Memo Chapter 6
Rails Tutorial Chapter 1 Learning
Rails Tutorial Chapter 2 Learning
Rails Tutorial 4th Edition: Chapter 1 From Zero to Deployment
[Rails Struggle/Rails Tutorial] Summary of Rails Tutorial Chapter 2
rails tutorial chapter 10 summary (for self-learning)
rails tutorial Chapter 6
rails tutorial Chapter 1
rails tutorial Chapter 7
rails tutorial Chapter 5
rails tutorial Chapter 10
rails tutorial Chapter 9
Rails Tutorial Chapter 0: Preliminary Basic Knowledge Learning 5
Rails tutorial (6th edition) Follow/unfollow background operation
Rails Tutorial Chapter 5 Notes
[Rails] Learning with Rails tutorial
Rails Tutorial Memorandum (Chapter 3, 3.1)
Rails Tutorial Chapter 8 Notes
Rails Tutorial Memorandum (Chapter 3, 3.3.2)
Rails tutorial (6th edition) Background operation of profile editing
[Rails Tutorial Chapter 4] Rails-flavored Ruby
Rails tutorial (6th edition) Background operation of password reset function
[Rails Tutorial Chapter 5] Create a layout
Chewing Rails Tutorial [Chapter 2 Toy Application]
rails tutorial
rails tutorial
rails tutorial
rails tutorial
rails tutorial
rails tutorial
[Rails Struggle/Rails Tutorial] Summary of Heroku commands
Rails tutorial (6th edition) Background operation of the posting function of the micro post
A summary of only Rails tutorial setup related
[Rails Struggle/Rails Tutorial] What you learned in Rails Tutorial Chapter 6
Rails tutorial test
[Rails Struggle/Rails Tutorial] What you learned in Rails Tutorial Chapter 3
[Ruby on Rails] Rails tutorial Chapter 14 Summary of how to implement the status feed
Rails tutorial memorandum 1
Rails Tutorial Chapter 1 From Zero to Deployment [Try]
Rails learning day 3
Rails tutorial memorandum 2
Rails learning day 4
Rails 6.0 Routing Summary
Rails learning day 2
Chewing Rails Tutorial [Chapter 3 Creating Almost Static Pages]
[Rails tutorial] A memorandum of "Chapter 11 Account Activation"
rails db: 〇〇 Summary
[Beginner] Rails Tutorial
[Learning Memo] Metaprogramming Ruby 2nd Edition: Chapter 3: Methods
rails learning day 1
Resolve Gem :: FilePermissionError when running gem install rails (Rails Tutorial Chapter 1)
[Ruby on Rails Tutorial] Error in the test in Chapter 3