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
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.
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.
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 routes
You can check the list of routes by using the command.
Exercise
get login_path
receives a login form with a GET request.
post login_path
Sends 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
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
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_bymethod
authenticate```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.
>> 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
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
In the case of Chrome, you can display the list with Cookies on the Application tab of the developer tools. ↑ The encrypted user ID is stored (_sample_app_session)
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.
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])`**
>> 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]>
>>
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.
Chrome cookie deletion screen After deleting, the login link is displayed.
Omitted. You will be prompted to log in when you restart your browser. (Temporary session disappears.)
We will test the behavior of dynamically changing header links with integration tests. The operation is
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_to
The redirect destination is@user
I'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_select
There is no login link in.
There is a logout link. Testing for show page links.
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
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
is_logged_in?
Method.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.
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.
It doesn't disappear when using Chrome, but the logout process is done.
debugger
You 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
Recommended Posts