I'm going through Michael Hartl's The Ruby on Rails Tutorial, Chapter 8.3 Logging Out Sessions and I don't understand how removing the session[:user_id] can remove the #current_user as well:
here is the SessionController:
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 will only flash once - if a new request or view is rendered,the flash will go away now
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_path
end
end
and here is the SessionsHelper for the login and logout helpers:
module SessionsHelper
def log_in(user)
session[:user_id] = user.id
end
def current_user
#find the user if #user is not defined yet, or else, just keep the current user
#that way we dont have to do a database search every time current_user is called
#current_user ||= User.find_by(id: session[:user_id])
end
def logged_in?
!current_user.nil?
end
def log_out
session.delete(:user_id)
end
end
The way I understand, once #current_user is defined once logged in, shouldn't the variable still last even though the session[:user_id] has been removed since it is being set to itself?
#current_user ||= User.find_by(id: session[:user_id])
There was no action that I am aware of that removed the #current_user variable. But when I tested it during the debugger, I can see that once someone logs out, #current_user becomes nil.
Can someone explain the mechanics to me?
The session persists between requests. But the instance variable #current_user only persists for the length of one request. When the destroy action redirects to the root_path that is the start of a new request which will load the root page.
You may want to try this out so see that clearing the user_id out of the session doesn't clear out the instance variable:
def destroy
# Test code to initialize #current_user
current_user
Rails.logger.debug("#current_user before: #{#current_user.inspect}")
log_out
# Test code to check #current_user after updating session
Rails.logger.debug("#current_user after: #{#current_user.inspect}")
redirect_to root_path
end
And then check what ends up in log/development.log. #current_user will still be present after log_out but it will go away at the end of the request.
Related
I'm trying to make the following work:
1) If I'm a user, I get signed out when I delete my account.
2) If I'm an admin, I stay signed in when I delete other user's account.
It works when I set the current_user method as
def current_user
#current ||= User.find(session[:user_id]) if session[:user_id]
end
but when I set it as
def current_user
User.find(session[:user_id]) if session[:user_id]
end
it gives me this error
ActiveRecord::RecordNotFound in UsersController#destroy
Couldn't find User with 'id'=30
I don't understand why "#current ||=" makes it work.
application_controller.rb
def require_signin
unless current_user
session[:intended_url] = request.url
redirect_to signin_path, notice: "This page requires signin"
end
end
def require_admin
unless current_user_admin?
redirect_to root_path, notice: "Unauthorized Access"
end
end
def current_user_admin?
current_user && current_user.admin?
end
def current_user
#current ||= User.find(session[:user_id]) if session[:user_id]
end
def current_user?(user)
user == current_user
end
user_controller.rb
def destroy
#user = User.find(params[:id])
unless current_user?(#user) || current_user_admin?
redirect_to root_path, alert: "Unauthorized Access"
end
#user.destroy
session[:user_id] = nil unless current_user_admin?
redirect_to players_path, alert: "'#{#user.name}' was deleted"
end
The ||= operator means "set this variable if its not already set".
When you delete the account that you are currently signed in as, you remove the User from the database, however the session[:user_id] is still set to the now deleted User's ID.
Attempting to call User#find with a user ID which has been deleted will result in an ActiveRecord error.
The reason why this does not happen when the ||= operator is present is because the #current variable is already set therefore the User#find is never called.
# this will only try to call User#find if the #current variable is not already set
#current ||= User.find(session[:user_id])
# this will always attempt to call User#find
# if session[:user_id] is set to a deleted user's ID it will raise an error
#current = User.find(session[:user_id])
I'm learning Rails and I'm trying to restrict access to pages if a user hasn't logged in and to only allow them to view the login and sign up pages.
Currently, my code creates a session when a user logs in and clears it when the user logs out. I've got a Sessions helper so that I can check whether a user is logged in but I'm unsure how to redirect the user throughout the app if he/she's not logged in.
UPDATE:
As I posted the question, I managed to get something to work with a before_filter. Should I use a before_action or before_filter?
Do I need to copy the same method in all my controllers where I want to restrict access?
CODE:
/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
/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
/helpers/sessions_helper.rb
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
#current_user ||= User.find_by(id: session[:user_id])
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
# Logs out the current user.
def log_out
session.delete(:user_id)
#current_user = nil
end
end
You can use a before_action. The rails guide has a nice section with an example on that:
class ApplicationController < ActionController::Base
before_action :require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access this section"
redirect_to new_login_url # halts request cycle
end
end
end
I have used omniauth-salesforce in my sessions controller I have following code, even when I logout the session is still present, I have tried session.clear and reset_session in destroy method but nothing works, sign in works only after clearing browser cache
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
end
How to delete session on signout?
Session is similar to a normal hash so below thing should work.
session.delete(:user_id)
Use this
session[:user_id] = nil if session[:user_id]
So on my local host, the sign in persists and the user is signed in. However, on Heroku, after signing in, it doesn't even recognize that the user is still signed in.
In the heroku logs, it starts
-A get request for log in
-SessionsController renders a new session
-A user is committed
-A POST request to start sessions occurs
-redirect occurs
-user sign in apparently isn't saved
-This seems like an issue with the cookie?
This is my Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
user = User.authenticate(params[:email], params[:password])
if user
session[:user_id] = user.id
sign_in user
redirect_to root_url, :notice => "Logged in!"
else
flash.now.alert = "Invalid email or password"
render "new"
end
end
def destroy
sign_out
redirect_to posts_url, :notice => "Logged out!"
end
end
This is my SessionsHelper
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def is_admin?
signed_in? ? current_user.admin : false
end
def sign_out
current_user.update_attribute(:remember_token,
User.encrypt(User.new_remember_token))
cookies.delete(:remember_token)
self.current_user = nil
end
end
In my user model I do save a remember_token before saving
before_save :create_remember_token
def create_remember_token self.remember_token = User.encrypt(User.new_remember_token)
Any ideas would be much appreciated!
Following your code step by step:
A User is created with no remember token.
The sign_in method is being called. Nil is saved to cookies as User has no remember token yet.
Your current_user method sets #current_user to nil because User.find_by(nil) returns nil, and doesn't raise an exception as User.find(nil) would.
While I'm not 100% sure this is the reason why your code is breaking (as I can't see what callbacks you've written), it certainly makes sense. If in your local environment you create a new user from scratch I assume that it'll break as well. The only place I see you defining a remember token for the user is in the sign_out method - if you were signed in while implementing the feature, signed out to test it, and signed back in, the code would seemingly work.
This issue can be fixed by using a callback to set the remember_token of the User on create via callback, or in the sign_in method.
As a side note, if you're following Hartl's implementation of sessions, I'd definitely revisit that as you made a fairly large error in how you handle the remember tokens. The encrypted token should be stored on your database, and the unencrypted one should be stored on the cookie. Then when using find_by, you should be encrypting that cookie to find the user in the database. As it currently stands you're saving the naked remember token in both the db and cookie, which poses security issues.
I'm following the tutorial on http://ruby.railstutorial.org
Specifically, chapter 9 (9.2.3)
http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-users#top
I've managed to get the part when a user will get prompted to login when accessing a restricted page then be redirected back to the restricted page after successfully logging in.
I'm trying to get it so that after one redirects to the protected page, the next login attempt will direct back to the main user profile page, however, session.delete(:return_to) doesn't appear to be working and the user is repeatedly directed back to the originally saved protected page. Here's my code:
My session Controller:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:session][:email])
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_back_or user
# Sign the user in and redirect to the user's show page.
else
# Create an error message and re-render the signin form.
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
...
end
My session helper:
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
def store_location
session[:return_to] = request.url
end
end
Any help you can give would be brilliant! It seems like session.delete() simply isn't working.
The following block solved it. Nothing else needs to change.
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in." #unless signed_in?
end
end
When I did the tutorial, my code had the first lines of the SessionsController#create method as just:
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
But, I can see that the corresponding code in the book has changed to:
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
I attempted to use that new code in my sample_app, but most of my tests ended up failing. So, for you, I guess test adding the downcase method to your params[:session][:email] call first, and if that doesn't work, try substituting the lines out for the session-less code above and see if it works.
Update
After looking at your code, as far as I can tell, these are your problems:
You're calling session.delete(:return_to) in SessionsController#create for some reason. This line can be removed:
app/controllers/sessions_controller.rb
def create
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# session.delete(:return_to)
sign_in user
# ...
#...
end
Both lines of code in your UsersController#signed_in_user method need to be put in the unless block, not just the call to redirect_to:
app/controllers/users_controller.rb
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in." #unless signed_in?
end
end
If you make these changes and run your tests, you'll still have a Nokogiri::XML::XPath::SyntaxError: on your call to
spec/requests/authentication_pages_spec.rb
it { should have_exact_title('title', text: full_title('')) }`
but I'm assuming this is a custom matcher you're planning to work on. If not and it's a mistake, remove it and all your tests will pass.