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])
Related
I am learning how to build a simple authentication function with session and cookies.
I have a User model, then sessions_controller. I also create remember_token column for users table to handle the "remember_me" situation.
This is the relevant code:
sessions_controller
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
if params[:remember_me]
user.generate_token(:remember_token)
cookies.permanent[:remember_token] = user.remember_token
end
redirect_to root_url
flash[:notice] = "Logged in."
else
render "new"
flash[:notice] = "Invaid email or password."
end
end
user.rb
has_secure_password
def generate_token(column)
self[column] = SecureRandom.urlsafe_base64
self.save
end
application_controller
helper_method :current_user
def current_user
if session[:user_id]
#current_user ||= User.find(session[:user_id])
elsif cookies[:remember_token]
#current_user ||= User.find(cookies[:remember_token])
end
end
The code above seems to be working. But at first I actually reversed the code's sequence in :current_user, like this:
def current_user
if cookies[:remember_token]
#current_user ||= User.find(cookies[:remember_token])
elsif session[:user_id]
#current_user ||= User.find(session[:user_id])
end
end
This will raise an exception: "Couldn't find User with 'id'= some_token_here"
In this case, why I can't use the cookies[:remember_token] to find the user firstly. Does the sequence matter? Or, if I had some misunderstanding about how session and cookies work?
That is because if you use find, it finds a record by its database id.
What you want is to find by the remember_token column, so User.find_by(remember_token: cookies[:remember_token])
def current_user
if cookies[:remember_token]
#current_user ||= User.find_by(remember_token: cookies[:remember_token])
elsif session[:user_id]
#current_user ||= User.find(session[:user_id])
end
end
From Rails Guide
Using the find method, you can retrieve the object corresponding to the specified primary key that matches any supplied options.
For example:
# Find the client with primary key (id) 10.
client = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">
User.find(session[:user_id]) returns all users that has matching primary_key
In your second case: "User.find(cookies[:remember_token])", you should
search from "remember_token" column.
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.
I am new to programming and getting the following errors while trying to get through the 9th chapter of Rails Tutorial. I checked the code several times but still didn’t understand why my local variable or method isn’t being defined. Every time I rewrite the code I get similar errors: undefined local variable or method ‘current_user’.
Error:
ERROR["test_layout_links", SiteLayoutTest, 0.888518]
test_layout_links#SiteLayoutTest (0.89s)
ActionView::Template::Error: ActionView::Template::Error: undefined local variable or method `current_user' for #<#<Class:0x007fcd97c44cf0>:0x007fcd97c4c4a0>
app/helpers/sessions_helper.rb:22:in `logged_in?'
app/views/layouts/_header.html.erb:8:in `_app_views_layouts__header_html_erb__1982327839123609485_70260496954760'
app/views/layouts/application.html.erb:12:in `_app_views_layouts_application_html_erb___2753884707929057206_70260450931560'
test/integration/site_layout_test.rb:6:in `block in <class:SiteLayoutTest>'
app/helpers/sessions_helper.rb:22:in `logged_in?'
app/views/layouts/_header.html.erb:8:in `_app_views_layouts__header_html_erb__1982327839123609485_70260496954760'
app/views/layouts/application.html.erb:12:in `_app_views_layouts_application_html_erb___2753884707929057206_70260450931560'
test/integration/site_layout_test.rb:6:in `block in <class:SiteLayoutTest>'
sessions_controller
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)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination' #Not quite right!
render 'new'
end
end
def destroy
log_out if logged_in?
redirect_to root_url
end
end
users_controller
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
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
def edit
end
def update
if #user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to #user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# Before filters
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# Confirms the correct user.
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
end
sessions_helper
module SessionsHelper
#logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
#Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
#Returns the user corresponding to the remember token cookie.
def current_user?(user)
user == current_user
end
#returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
#Forgets a persistent session.
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
#logs out the current user.
def log_out
forget(current_user)
session.delete(:user_id)
#current_user = nil
end
# Redirects to stored location (or to the default).
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# Stores the URL trying to be accessed.
def store_location
session[:forwarding_url] = request.url if request.get?
end
end
sessions_helper_test
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
def setup
#user = users(:michael)
remember(#user)
end
test "current_user returns right user when session is nil" do
assert_equal #user, current_user
assert is_logged_in?
end
test "current_user returns nil when remember digest is wrong" do
#user.update_attribute(:remember_digest, User.digest(User.new_token))
assert_nil current_user
end
end
I think you may be missing the current_user method. It's defined here.
Though the name is similar, current_user is a completely different method to current_user?. The question mark is a convention that typically means the method will return either true or false.
You're missing a current_user method in your sessions_helper.rb. Check back in Chapter 8.57
I have 2 models, users and common_app.
Users has_one common_app.
In the common_app controller, I define almost everything using the current_user helper. This essentially makes it so that the edit forms ignore the id that the user POSTs via the web browser.
It looks like so -->
class CommonAppsController < ApplicationController
before_action :signed_in_user
def new
if current_user.common_app.present?
redirect_to current_user
else
#common_app = current_user.build_common_app
end
end
def create
#common_app = current_user.build_common_app(common_app_params)
if #common_app.save
flash[:success] = "Common App Created!"
redirect_to root_url
else
redirect_to 'common_apps/new'
end
end
def update
if current_user.common_app.update_attributes(common_app_params)
flash[:success] = "Common App Updated"
redirect_to root_url
else
render 'common_apps/edit'
end
end
def show
#common_app = current_user.common_app
end
def edit
#common_app = current_user.common_app
end
private
def common_app_params
params.require(:common_app).permit(:current_city,:grad_year,:read_type,
:listen_speak,:time_in_china,
:cover_letter,:resume) ####fill in the correct ones here
end
# is correct_user necessary?
end
What makes me wary though is that I am not using a correct_user before action. If I were to not use it, would there be a security hole here? I.e could someone POST through a shell or something?
If yes, how would you change the controller, to include the before filter?
PS: I'm also a bit confused about the correct use of # variables. If I am overusing them, or doing something wacky with them, please let me know and help me become a better noob :)
PPS: This is my SessionsHelper Module, for the signed_in_user before filter to work -- >
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(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
remember_token = User.encrypt(cookies[:remember_token])
#current_user ||= User.find_by(remember_token: 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) # this creates friendly forwarding for the app
redirect_to(session[:return_to] || default)
session.delete(:return_to)
end
def store_location
session[:return_to] = request.url if request.get?
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
end
I don't see any security problem here. Even without the before_action :signed_in_user, since you always go through the current_user.common_app association, if a user where not signed in, the action would simply fail.
So the controller is sound. As long as your authentication system has no flaws, the controller itself exposes no weakness.
In Ruby, variables prefixed with '#' are instance variables. In the context of a Rails controller, the distinction is simple:
Use instance variables for values you want to make available to your view, and normal variables for everything else.
My question is actually fairly simple, how do I make a create action which checks if a user is logged in, and if she/he is then redirect to the dashboard instead of rendering the index page where they've got links and stuff to go to and sign up. Also why is the code below not working.
class UsersController < ApplicationController
def new
#user = User.new
end
def create
if current_user.nil?
redirect_to dplace_index_path
if current_user
#user = User.new(params[:user])
if #user.save
auto_login(#user)
redirect_to dplace_index_path
end
end
end
end
end
Your code isn't doing what you expect because the if statements are actually nested (you want elsif with this same structure -- or see my suggested fix below). Here's what your code, when properly formatted, actually looks like:
def create
if current_user.nil?
redirect_to dplace_index_path
if current_user
#user = User.new(params[:user])
if #user.save
auto_login(#user)
redirect_to dplace_index_path
end
end
end
end
Logically, you will never get down into the second if statement, because current_user must be nil to enter the first. Try something like this instead:
def create
if current_user
#user = User.new(params[:user])
if #user.save
auto_login(#user)
redirect_to dplace_index_path
end
else
redirect_to dplace_index_path
end
end
I rearranged the code, but it should logically do what you want now. I put the "happy path" first (the current_user exists), and moved the redirect into the else statement.
General user authentication:
def create
user = User.find_by_email(params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to dashboard_url, :notice => "Logged in!"
else
flash.now.alert = "Invalid email or password"
render "new"
end
end
Try:
def create
if current_user.blank? # .blank? will check both blank and nil
# logic when user is not logged in
redirect_to index_path
else
# logic when user is logged in
redirect_to dashboard_path
end
end
def create
redirect_to dplace_index_path unless current_user
# no need to check current_user again
#user = User.new(params[:user])
if #user.save
auto_login(#user)
redirect_to dplace_index_path
end
end