So I'm following the Rails Tutorial, and I've gotten to the portion where we want to sign a user in with a sign_in SessionHelper.
Question 1:
module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user
end
What I'm having difficulty with is the part that reads:
The problem is that it utterly fails to solve our problem: with the code the user's signin status would be forgotten: as soon as the user went to another page.
I don't understand how this is true? I read on and understand the added code makes sure #current_user is never nil. But I'm not seeing how current_user would revert to nil if we just established it in 5th line.
Question 2:
The updated code reads as such:
module SessionsHelper
def sign_in(user) #in helper because used in view & controller
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
current_user = user
end
def current_user=(user) #set current_user
#current_user = user
end
def current_user #get current_user
#current_user ||= user_from_remember_token #<-- short-circuit evaluation
end
private
def user_from_remember_token
User.authenticate_with_salt(*remember_token) #*=use [] instead of 2 vars
end
def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end
In the remember_token helper, why does it use cookies.signed[] instead of cookies.permanent.signed[] & why doesn't it use ||= operator we just learned about?
Question 3:
Why do we need to authenticate_with_salt? If I authenticate & sign_in can see the id & salt attributes from the user who was passed to it, why do we need to double_check it? What kind of situation would trigger a mixup?
Remember that instance variables like #current_user are only set for the duration of the request. The controller and view handler instances are created specifically for rendering once and once only.
It is often easy to presume that because you've set a variable somewhere that it will continue to work at some point in the future, but this is not the case. To preserve something between requests you need to store it somewhere, and the most convenient place is the session facility.
What's missing in this example is something along the lines of:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
Generally it's a good idea to use the write accessor to map out the functionality of the sign_in method you've given as an example:
def current_user=(user)
cookies.permanent.signed[:remember_token] = [user.id, user.salt]
#current_user = user
end
It's odd that there is a specific "sign in" method when the act of assigning the current user should be the same thing by implication.
From a matter of style, though, it might be more meaningful to call these methods session_user as opposed to current_user for those situations when one user is viewing another. "Current" can mean "user I am currently viewing" or "user I am currently logged in as" depending on your perspective, which causes confusion. "Session" is more specific.
Update:
In response to your addendum, the reason for using cookies to read and cookies.permanent to assign is much the same as using flash.now to assign, and flash to read. The .permanent and .now parts are intended to be used when exercising the assignment operator.
Related
I'm a newbie working my way through Hartl's Rails 5 tutorial when I came across this piece of confusing code listed below. It looks like #current_user is being defined within a method as an instance variable, so that it may be used outside the method. However, in the logged_in? method, no # symbol is needed to call on it. In fact, if I add rewrite logged_in? to use #current_user, it no longer works as intended!
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
if session[:user_id]
#current_user ||= User.find_by(id: session[:user_id])
end
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
end
When I try this in ruby, I'm unable to use an instance variable in this way. The example I tried is below. When I call method2, I get an error that isn't fixed until I add an # symbol in front of var1. Can anyone help me understand this? Thanks!!
def method1
#var1 = 2
#var1
end
def method2
#var1 == 2
end
puts method1
puts method2
current_user is being defined as a method, and then inside the method, an instance variable #current_user is defined, which is only used inside the method. The logged_in? method is calling the current_user method, it is not accessing the #current_user variable directly. Note that in ruby, the #, $, or ## are actually part of the variable name, and are not modifiers. Therefore, current_user and #current_user are not the same entity.
In Michael Hartl's rails tutorial we have a current_user method defined as such:
# Returns the user corresponding to the remember token 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?(:remember, cookies[:remember_token])
log_in user
#current_user = user
end
end
end
At first I assumed #current_user was needed as opposed to some other local variable like the_current_user (assuming you can't use current_user since that's the name of the method.
When using helper methods that return something, do we need an # variable or can we just use any variable type? (I saw other helper_methods use normal variable_names). (Im assuming #current_user was just convenient)
# makes variables available throughout the class.
If was just current_user instead of #current_user it would only be accessible inside of that specific method.
You can use current_user as a variable name inside of a method named current_user
you don't need to use # before an instance variable to return a value.
def some_method
user = "Jimmy"
user
end
> puts some_method
"Jimmy"
Here's a common Ruby idiom, up close:
def current_user
#current_user ||= User.find_by(id: something)
end
You can call current_user as often as you like, now, because it will only spend time hitting the database the first time you call it. On subsequent calls, #current_user has a value, so the ||= evaluates trivially as #current_user = #current_user.
The effect lasts as long as the # instance exists. If it's a controller, it will last for the current action, and then disappear. This means a new action with a different user will not trip over the previous value of #current_user.
Because def current_user occupies a namespace different from #current_user, the Ruby idiom is to name the "memento pattern" variable the same as the method it optimizes.
Your example makes the memento pattern a little confusing, because there are two ways to generate a current user.
You can return normal variable in helper methods.
# is just a syntax used to define an instance variable.
#current_user is just a convention, you can use any name like #whatever_user
For example, if we are using current_user instead of #current_user then you will not be able to access current_user from any of your views.
For example, if you want to show the name of the user if the user is signed in:
<%= #current_user.name if #current_user %>
It is possible only because we have used the instance variable.
Following will help you in knowing more about instance variable:
https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/instancevars.html
http://ruby-for-beginners.rubymonstas.org/writing_classes/instance_variables.html
To know about all the types of variables available in Ruby:
https://www.studytonight.com/ruby/types-of-variables-in-ruby
Side Note
#current_user ||= User.find_by(id: user_id)
This pattern is called memoization. It is a very common pattern in Ruby/Rails. You can read more about it here:
https://www.justinweiss.com/articles/4-simple-memoization-patterns-in-ruby-and-one-gem/
I am learning Rails from Michael Hartl's tutorial, but I am really confused about the SessionsHelper module. Not enough information is provided about the duplication of the current_user method. Could someone explain why there are two and what are their individual purposes?
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
end
I understand the sign_in method would trigger the call to current_user=(user) but why the current_user method again? I understand the second method gets the user based on remember_token from the database, but I can't connect the dots regarding these.
current_user is the reader method and current_user= is the writer method for your attribute current_user. These two are separate methods, one would be used to read the value and other to write value of current_user.
EDIT
In your case, current_user= method means that set the value of instance variable #current_user equal to the passed value(user).
def current_user=(user)
#current_user = user
end
current_user method means,
def current_user
## if #current_user = nil then
## set #current_user = User.find_by_remember_token(cookies[:remember_token])
## else return #current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
sign_in method,
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
sign_in method would be called as sign_in user from a controller.
Like Aaron mentioned in the comment, without self, Ruby would simply create a local variable called current_user which would be lost once sign_in method finishes execution.
But by saying, self.current_user = user the value of current_user would be preserved in the current instance(instance of controller in which you have included the module) of class.
For a better understanding of self refer to this Stackoverflow question on self
Why is the middle method necessary? It seems to me like it's just an intermediary step to connect the first and third methods.
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
end
It is the setter method or the helper method to set the current_user with the user who is currently online. This is used by devise gem to identify the current user.
So whenever you need to find the online user for your application, you just use the following code-
if current_user
#Do something important
else
#You do not have enough privileges. Please login.
#Your offline stuff
end
I thought methods such as name and email were default in rails?
In my static pages view, in profile.html.erb I have:
<% if logged_in? %>
<% provide(:title, #user.name) %>
<% else %>
<% provide(:title, 'Profile')%>
<% end %>
I put in my static_page_controller
def profile
#user = User.find_by_remember_token(:remember_token)
end
When I go to the console User.find_by_remember_token("actualtoken").name returns me the appropriate users name, but :remember_token does not. How do I make :remember_token = the logged in users remember token?
In my sessions_helper I have
def log_in(user)
cookies.permanent[:remember_token] = user.remember_token
current_user = user
end
def logged_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= user_from_remember_token
end
def log_out
current_user = nil
cookies.delete(:remember_token)
end
private
def user_from_remember_token
remember_token = cookies[:remember_token]
User.find_by_remember_token(remember_token) unless remember_token.nil?
end
end
copying it to my static_pages_helper didn't accomplish anything.
Quick things you should be aware of the rails framework and the ruby language:
A function defined in any of your helpers will be available to all helpers and views (so there is no reason to copy and paste the same functions through different helpers);
You're probably using an authentication gem and I guess it is the Devise gem. If this is right, then you should not be overriding their helpers unless you have a reason to do this;
User.anything will call the static function anything from the User class;
user = User.find_by_anything(the_thing) is a class static helper provided by ActiveModel that will query the database looking for a user that has *anything = the_thing*; this user or nil will be returned;
user.an_attribute will call a function that returns the user specified attribute (which is the same as the column name of this attribute by default);
user.try(:anything) will try to call the function anything from the user and return its value. If user is nil, the returned value will also be nil.
That said, I guess you just wanted to retrieve the current user remember token, which can be accomplished with the following:
user = current_user.try(:remember_token)
EDITED: The question is a bit messy, but I also think the following code will work with your controller:
def profile
#user = User.find_by_remember_token(params[:remember_token])
end
You must access the request's parameters through the params hash.
EDIT: completely replaces my first answer with one hopefully not as stupid :-)
(While there are several ways to implement and manage sessions in Rails, the default uses a cookie in the browser to reference a key stored in memory. Sessions are created by a request from a browser, so while it's certainly possible to use the console to get at an existing session, it's probably not what you want.)
So your method, user_from_remember_token will either return a user or nil. What I don't see in your code is where you're setting the remember_token on the User model. I'll assume it's there, but you may want to have code that tells the user to log in if you don't find them. A common pattern would be
def current_user
#current_user ||= user_from remember_token
unless #current_user
flash[:notice] = "Yo! Log in first."
redirect_to login_path and return
end
end
There's no problem calling a model finder from a separate controller. But why call User.find_by_remember_token(:remember_token) -- you don't have the remember_token yet (right?). Don't you just want to call the current_user method in your sessions helper?
If the method is not visible, you may want to include or require the session helper in your application_controller.rb