Adding to / overwriting Devise's current_user method - ruby-on-rails

I've got a User model, that has an associated keyholder and guest. To make my code more DRY, I'd like it so that current_user will refer to:
a) If a user is signed in, it will be the normal Devise current_user object.
b) If a keyholder is signed in, it will be that keyholder's user - i.e. current_keyholder.user
c) If a guest is signed in, it will be the guest's user.
I've tried adding helpers into the application controller, however, these don't work as the e.g. current_user isn't being initialized before my code runs.
Basically what I'm trying to achieve is:
def current_user
if user_signed_in?
#current_user ||= warden.authenticate(:scope => :user)
elsif keyholder_signed_in?
#current_user = current_keyholder.user
elsif guest_signed_in?
#current_user = current_guest.user
end
end
The above code gives me a "stack level too deep" error, but will hopefully show what I'm trying to get working. If anyone can help, that would be great, thanks!

I found a workaround - in the application controller, I added a before_filter called access_user, then a method called access_user:
def access_user
if user_signed_in?
#access_user = current_user
elsif keyholder_signed_in?
#access_user = current_keyholder.user
elsif guest_signed_in?
#access_user = current_guest.user
end
end
Which I can then call in the views using e.g. <%= #access_user.id %>
If there is a better way or this is going to cause some security issue, please can somebody let me know?

Related

Session Helper Methods in Michael Hartl Tutorial Chapter 8

I'm going through Michael Hartl tutorial on Ruby on Rails and I'm having trouble understanding some logic. Note that the logic works, it's just not resonating with me on what's actually happening.
In this chapter we're logging in users and creating a session. Here the helper methods:
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
end
Depending on whether the user's logged on or not we change the navigation with this conditional:
<% if logged_in? %>
do something....
<% else %>
do something else...
<% end %>
As I understand it, the code checks to see if the user is logged in by calling the logged_in? method. The logged_in? method calls the current_user method to see if it's nil. If it is nil it returns false, if it's not nil it returns true. As a test I tried to change the logged_in? method to the following:
def logged_in?
!#current_user.nil?
end
When I run this method for some reason, after I log in with credentials that are authenticated, #current_user returns nil. Why is this? Note this works if I change it back to the original logged_in? method where I'm just calling the method current_user.
This is not a direct answer to your question since you have that figured it out while I was trying to answer. But I want to clarify on some points.
In rails, in fact most of the web application, we track a user's log in state in server's session. the log_in method in your code does that.
Then when the new request comes in to a controller that requires authentication, we check the session if there is a stored user. If it exists then the request is authenticated, else it's unauthenticated. So, logged_in? method's actual responsibility is to check the session.
However, it is quite common that we want to access the authenticated user's attributes in the controller and/or views. So we set an #current_user variable on the controller so that you can access the User object of authenticated user. Again, using an instance variable directly is not a good practice. So we wrapped it in the current_user method.
Then you might ask, why don't we store the whole user object in session? Because it is bad to store much in session(see here). So, we just store the id and use it to get the user from db.
Here is where the ||= part comes in. ||= caches the result of db. Otherwise, we would be hitting db every time we call current_user method.
Hope this clarifies a bit on what's actually happening.
As I was formulating my question I figured this out. In the later case
def logged_in?
!#current_user.nil?
end
#current_user isn't set yet because the current_user method was never called. To test, I changed the method to the following:
def logged_in?
current_user
!#current_user.nil?
end
where the method was called first and then #current_user was evaluated. It worked without issue. The original method works because current_user returns #current_user to the logged_in? method as either a user object or nil (#current_user is set as the last line in the method -- it's the only line, so it's retuned implicitly to the logged_in? method).

Can I use CanCan with model(s) other than User?

I'm new to rails and building app from data on an api, I have two tables I want to use for users - students and educators.
I can authenticate in the controllers with
private
def fetch_user_data(username, password)
require 'URI'
uri = URI('the url for the api')
res = Net::HTTP.post_form(uri, 'username' => username, 'password' => password)
xml = res.body
doc = Nokogiri::Slop(xml)
#status = doc.auth.status.content.to_s
#username = doc.auth.username.content.to_s
#token = doc.auth.token.content.to_s
#person_id = doc.auth.person_pk.content.to_i
#security_roles = doc.auth.security_roles.content.to_s.downcase
end
def assign_user
if /faculty/ =~ #security_roles
#user = Educator.find_by(person_id: #person_id)
elsif /student/ =~ #security_roles
#user = Student.find_by(person_id: #person_id)
end
end
def authenticate_user(username, password)
fetch_user_data(username, password)
assign_user
session[:user_id] = #user.id
redirect_to #user
end
Now I know it's probably not pretty, but I'm learning as I go. I use the authenticate_user() in the sessions controller, and based on the redirect, the authentication seems fine. I do have one question about the :user_id key in the session -- is that a key only created for the session or is it trying to pull a value from a user table? Knowing that would help. My guess is it's just created for the session, but I have no idea.
Ok so now for my real problem. I'm trying to use cancan and I'm getting stuck at defining current user.
I figured I could have the #current_user instance point to the #user I assigned in assign_user. This doesn't seem to be working though. I tried a couple things, but I'm stuck. Maybe I can't even do that? Larger question? Do I have to have a user model to make cancan work? Can I use the two models Educators and Students and mask the user references in cancan on those?
I tried this, but it's not working -- any help?
def current_user
#current_user ||= #user
end
edit: Figured out a bit.
1. methods were in ApplicationHelper. Moved current_user() to ApplicationController . Changed syntax to conventional, but added conditionals.
def current_user
if Student.where(id:session[:user_id]).count == 0
#current_user ||= Educator.find(session[:user_id])
else
#current_user ||= Student.find(session[:user_id])
end
end
This seems to fix things, and allows me to use both tables as the user models.
There are several ways to define current_user (if you aren't using Devise), but this one here is pretty standard:
class ApplicationController < ActionController::Base
def current_user
#current_user ||= User.find(session[:user_id])
end
end
As for your question about the session, the way you have it set up, you're setting session[:user_id] equal to #user.id (which is always the same for each user).
minor aside consider using the gem CanCanCan which is as it sounds. CanCan not maintained and some one set up replacement CanCanCan. You won'thave to change any code.

Rails - calling an instance variable from anywhere in the app

I am fairly new to Rails, so apologies if it's not an 'instance variable' I am talking about!
I am using Devise for authentication, so can use things like current_user throughout the app. The app I am building has a User model, but also a Keyholder model (who is a sort of moderator for that user), and a Guest (who has read-only access to some things for that user).
What I want to know is - can I set it up so that I can use e.g. access_user when logged in as the keyholder to access the same object as current_user - and if so, where do I put the code in my app? It's quickly becoming very verbose and un-Rails-like having to repeat myself otherwise.
What I am trying to achieve is being able to use 'access_user' instead of current_user, so that regardless of whether it is the user, keyholder or guest logged in, it will use the user object.
For example:
def access_user
if user_signed_in?
access_user = current_user
end
if keyholder_signed_in?
access_user = current_keyholder.user
end
if guest_signed_in?
access_user = current_guest.user
end
end
Thanks!
Class level instance variables could also help you.
def access_user
if user_signed_in?
#access_user = current_user
end
if keyholder_signed_in?
#access_user = current_keyholder.user
end
if guest_signed_in?
#access_user = current_guest.user
end
end
You can just set this method in ApplicationController, and expose it to a helper method.
class ApplicationController
helper_method :access_user
def access_user
#blah blah
end
end
When method in ApplicationController, it's available to all controllers.
When you use helper_method, it is exposed as helper method to be used in View. More about helper_method: http://apidock.com/rails/ActionController/Helpers/ClassMethods/helper_method

Why am I getting error Undefined method `name' for nil:NilClass with Ruby on Rails?

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

Rails Tutorial — 9.3.3 Current_User

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.

Resources