Rails 3 + Devise: user_signed_in? for a different user in the database? - ruby-on-rails

I'm building an app where I want to add an online status for a given user.
I know that Devise has a method user_signed_in? built in to check if the user who is using the app is signed in or not. But when I try to use it for a different user like this:
user_signed_in?(user)
user.user_signed_in?
I obviously get an undefined method error.
Does Devise have a method for this or do I have to write my own?
One approach was to store the online status of a given user in the user model.
What's the best solution to this?

I have used Devise on my applications and experienced some of the same problems as you when I first began working with it. You are merely using the helper methods incorrectly. If you'd like to check if the current user has a session and is signed in, you use the helper as such:
if user_signed_in?
which is essentially the same statement as:
if !current_user.nil? && current_user.signed_in
If you'd like to check if a user object is signed in, then you call this: (where user is a User Model Object)
if user.signed_in?

I'm not the author of Devise, but from what I can tell of Warden / Devise neither keep track of who is logged in.
The problem with having an is_online column in the User table is that it is difficult to see who is active on the website. I would add a column to your User model called last_seen as a date-time, and update that with Devise every time the user requests a page. You could then easily add a User.online_count method or also see if a user has been seen at the website in the last 5 minutes.

Use devise_for :user in your routes.rb.

Related

How can I allow Devise users to log in when they're outside my default scope?

I have a Rails 4 app which uses Devise 3.4 for authentication, which I've customized with the ability to ban users (using a simple boolean column users.banned, default false). The User model also has a default_scope which only returns non-banned users.
Here's the problem - I still want my banned users to be able to log in, even though they can't do anything after logging in. (They essentially just see a page saying "you've been banned"). But it seems that the default_scope is tripping up Devise. When you log in or call e.g. authenticate_user!, Devise tries to find the current user using one of the basic ActiveRecord methods like find or find_by, but can't because they lie outside the default scope. Thus Devise concludes that the user doesn't exist, and the login fails.
How can I make Devise ignore the default scope?
After a long time digging around in the Devise and Warden source code, I finally found a solution.
Short Answer:
Add this to the User class:
def self.serialize_from_session(key, salt)
record = to_adapter.klass.unscoped.find(key[0])
record if record && record.authenticatable_salt == salt
end
(Note that I've only tested this for ActiveRecord; if you're using a different ORM adapter you probably need to change the first line of the method... but then I'm not sure if other ORM adapters even have the concept of a "default so
Long Answer:
serialize_from_session is mixed into the User class from -Devise::Models::Authenticatable::ClassMethods. Honestly, I'm not sure what it's actually supposed to do, but it's a public method and documented (very sparsely) in the Devise API, so I don't think there's much chance of it being removed from Devise without warning.
Here's the original source code as of Devise 3.4.1:
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
The problem lies with to_adapter.get(key). to_adapter returns an instance of OrmAdapter::ActiveRecord wrapped around the User class, and to_adapter.get is essentially the same as calling User.find. (Devise uses the orm_adapter gem to keep it flexible; the above method will work without modification whether you're using ActiveRecord, Mongoid or any other OrmAdapter-compatible ORM.)
But, of course, User.find only searches within the default_scope, which is why it can't find my banned users. Calling to_adapter.klass returns the User class directly, and from then I can call unscoped.find to search all my users and make the banned ones visible to Devise. So the working line is:
record = to_adapter.klass.unscoped.find(key[0])
Note that I'm passing key[0] instead of key, because key is an Array (in this case with one element) and passing an Array to find will return an Array, which isn't what we want.
Also note that calling klass within the real Devise source code would be a bad idea, as it means you lose the advantages of OrmAdapter. But within your own app, where you know with certainty which ORM you're using (something Devise doesn't know), it's safe to be specific.

Setting Pundit role for user from Devise Registrations New View / Controller

I have both Pundit and Devise setup and working correctly in my rails app. However I am unsure about how to let the user decide their role when signing up.
At the moment:
I have a URL param which is passed to the Devise new view.
In the form_for I set a hidden field called role to the value of the param.
This works.
But I am concerned that a malicious user could change this param to say "Admin" and now they are an admin.
How should I handle this? I don't want to put a restriction in the model as that will cause issues when I want to create an admin. Should I override the devise registrations controller to put a check in there?
You don't need to override Devise's RegistrationsController for what you're trying to do.
If you want admins to be able to create users that have an arbitrary role set, you could simply use your own controller. Devise still makes it easy to create a user yourself, so you'll just have to make a controller handling this. Of course, don't forget to protect it using Pundit so only admins can use this functionality.
This approach still works if you use the Confirmable module. As no confirmation e-mail will be sent on user creation, though, you'll either have to call user.confirm! after saving the model to immediately unlock the account, or manually send the confirmation e-mail using user.send_confirmation_instructions.
Edit:
This Pundit policy may or may not work for what you're trying to do. You will have to override the create action of Devise's RegistrationsController here in order to use Pundit's authorize method. For dryness' sake, you should also move the roles list elsewhere, perhaps into the model.
class UserPolicy < Struct.new(:current_user, :target_user)
def create?
registration_roles.include?(target_user.role) if current_user.nil?
end
private
def registration_roles
%w(RED BLU Spectator)
end
end
After a fair amount of googling I have an answer. First stick some validation in your model for the roles Active Record Validations Guide: See 2.6 inclusion: validator option
After this your roles are validated to ensure they are correct, you could of course have a lookup table as well. Then you have two options:
Use a conditional before_save Callback for new records. Then check if the new record has the role your protecting and if so raise an error. To catch later (in an overridden devise controller (see second option).
Override the Devise registrations controller See this SO question. And put some checks in a completely overridden create action. Use the session to store the url param passed to the new action (also needs to be completely overridden). Then if the create action fails and redirects to new you still have access to the role in the session (as the param will be cleared from the URL unless you manipulate it).
So either way you need to override the registrations controller, its just a case of how much.
I suspect there is a way to do this with just Pundit. But I have yet to be able to get it to work.

customizing Devise's authenticate and current_user methods to work with soft delete / acts as paranoid

I am using Devise as authentication for a rails 3.2 app. My user model is called User, and I've been trying to implement a soft delete. I am trying to do this by using acts_as_paranoid, which automatically does the work for you. Things work as expected (others can no longer see the "deleted" user), except that I want the deactivated user to still be able to log in and see a "deactivated" screen, and give them the chance to reactivate their account.
The problem is that Devise (Warden?) is no longer able to find the deleted user. Acts_as_paranoid lets you access the soft deleted records if you use the scope "with_deleted". So I am able to get part of the way there with:
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
where(conditions).with_deleted.first
end
I put this into my user model, and so now when I input the log in information, I'll get the flash message that I have successfully logged in, and it'll touch the "updated_at" column in the user model and increment the sign_in_count, etc. However, it doesn't really authenticate in the sense that the authenticated method returns false and the current_user helper method returns nil. So what is my best strategy to get these working? Can I override the current_user method somehow so that it queries the User model with the with_deleted scope? Do I have to do something with warden, such as added conditions like I did with the find_first_by_auth_conditions method? I cannot figure out how to do this. Any help is appreciated!
also, after I do get it working, I would like to automatically send all soft deleted users to a "deactivated" page where their only options are to permanently delete or reactivate. Is there some way to do this with routing via the "authenticated :user do {} end", or do i have to put a before_filter in the application_controller and check for the users at every request?
I can answer your last question, about routing to the "deactivated page". You can use a lambda to isolate different types of users, like this:
authenticated :user, lambda {|u| u.deactivated? } do
root :to => 'application#deactivated'
end
authenticated :user, lambda {|u| u.active? } do
root :to => 'application#active'
end
Thanks to a tip from the Devise group, it turns out I needed to override the serialize_from_session method, which is inside the authenticatable module and looks like this:
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
I'd been trying with no success to override modules using initializers (I was trying to override existing strategies, and also to try writing a new one for warden); but I kept getting weird name errors. I still haven't figured that out. However, with the tip, I went ahead and overrode the method inside my User model. Since I don't foresee using some other resource, I didn't mind just editing it like this. The code was just:
def self.serialize_from_session(key, salt)
record = with_deleted.find(key).first
record if record && record.authenticatable_salt == salt
end
This skips the to_adapter.get method altogether (which, to anyone interested, is in orm_adapter, not devise; this took me awhile to find as I was thinking I needed to override this). This probably isn't the cleanest way to do this, but it works well enough for me. And even if I do have a different resource, this should only override the User resource, so I think everything would work fine otherwise.
If problems crop up, I'll add them here. If not, hopefully this helps someone in some way. Certainly took me long enough to get to the bottom of!

How can I find a devise user by it's session id?

Given session["session_id"] is it possible to find the logged in User to which that session belongs to?
At least on my system (rails 3.2, devise 2.0.4), you can do it like this:
session is:
{"session_id"=>"be02f27d504672bab3408a0ccf5c1db5", "_csrf_token"=>"DKaCNX3/DMloaCHbVSNq33NJjYIg51X0z/p2T1VRzfY=", "warden.user.user.key"=>["User", [3], "$2a$10$5HFWNuz5p6fT3Z4ZvJfQq."]}
session["warden.user.user.key"][1][0], then is 3.
So, I'd find it as:
User.find(session["warden.user.user.key"][1][0])
I'm not sure what you are trying to accomplish but will try to answer
If you want only the User from the current session, a simple way would be to store his id on session, for example:
def login(username, pass)
# do the usual authentication stuff and get user
if logedin
session[:user_id] = user.id
end
end
Then get the current user would be something like
current_user =User.find(session[:user_id])
If what you want is finding all the users that are currently logged in, I think you need to config your app to save session at DB, because predefined is that session data is store in cookies in the browser. For more information on this check this answer
How to track online users in Rails?
EDIT: Just noticed you are using devise so the first way is actually there for you. You just need to call current_user method at any controller.
For the second way check this answer "Who's Online" using Devise in Rails
And i might add this, as i was trying to do it the other way, if you are using ActiveRecord session_store you can find all stored sessions of a user like so:
ActiveRecord::SessionStore::Session.select{ |s| s.data["warden.user.user.key"] && s.data["warden.user.user.key"][0][0] == user_id }

rails devise hook into on_login

I'm looking to hook into devise after login / after session create. How would I go about doing this?
Basically I want to set a users location every time they login, and to do that I need an after login hook of sorts.
Devise is built on Warden, so you can use Warden's after_authentication hook.
Put this in an initializer:
Warden::Manager.after_authentication do |user,auth,opts|
# do something with user
end
The remote IP address and other request info is stored in auth.request (i.e. auth.request.remote_ip).
See https://github.com/hassox/warden/wiki/callbacks
Devise updates the value of the user.current_sign_in_at timestamp on successful login. So, you could simply add a before_save filter to your User model. In that filter, check to see if the value of this field has changed, and if it has, set the users location.
BTW - I'm not sure what you mean by "location" - if you mean IP address, Devise already stores that for you.
Here's a page from the devise wiki: How To: Redirect to a specific page on successful sign in.
In summary, the recommendation is to add the following method to the application controller:
app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
custom_location_for(resource) || welcome_path
end
In the above code, resource means the object (user, account, etc) that you've implemented devise authentication for. (The object that has the devise_for in your routes.)

Resources