Gradual engagement, persistent guest user with Devise - ruby-on-rails

I'm trying to set up gradual engagement in my utility app which people can use without registering e.g. notepad.cc and jsfiddle.net and I plan to create a guest user (with Devise) for the user when he 'writes' to the app.
I found this guide on the Devise wiki https://github.com/plataformatec/devise/wiki/How-To:-Create-a-guest-user which shows how to create a guest user for the duration of the browser session. What I want is for the user to continue using the same guest account in subsequent visits, until he signs up, maybe when I introduce subscription plans for more features.
How can I modify what's in the guide to make this possible?
Code in the guide linked above:
# file: app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
# if user is logged in, return current_user, else return guest_user
def current_or_guest_user
if current_user
if session[:guest_user_id]
logging_in
guest_user.destroy
session[:guest_user_id] = nil
end
current_user
else
guest_user
end
end
# find guest_user object associated with the current session,
# creating one as needed
def guest_user
User.find(session[:guest_user_id].nil? ? session[:guest_user_id] = create_guest_user.id : session[:guest_user_id])
end
# called (once) when the user logs in, insert any code your application needs
# to hand off from guest_user to current_user.
def logging_in
end
private
def create_guest_user
u = User.create(:name => "guest", :email => "guest_#{Time.now.to_i}#{rand(99)}#email_address.com")
u.save(false)
u
end
end
And using it in the controller:
#thing.user = current_or_guest_user
#thing.save

After some yak-shaving I've managed to get it to work. Here's the working code:
class ApplicationController < ActionController::Base
protect_from_forgery
# if user is logged in, return current_user, else return guest_user
def current_or_guest_user
if current_user
if cookies[:uuid]
logging_in # Look at this method to see how handing over works
guest_user.destroy # Stuff have been handed over. Guest isn't needed anymore.
cookies.delete :uuid # The cookie is also irrelevant now
end
current_user
else
guest_user
end
end
# find guest_user object associated with the current session,
# creating one as needed
def guest_user
User.find_by_lazy_id(cookies[:uuid].nil? ? create_guest_user.lazy_id : cookies[:uuid])
end
# called (once) when the user logs in, insert any code your application needs
# to hand off from guest_user to current_user.
def logging_in
# What should be done here is take all that belongs to user with lazy_id matching current_user's uuid cookie... then associate them with current_user
end
private
def create_guest_user
uuid = rand(36**64).to_s(36)
temp_email = "guest_#{uuid}#email_address.com"
u = User.create(:email => temp_email, :lazy_id => uuid)
u.save(:validate => false)
cookies[:uuid] = { :value => uuid, :path => '/', :expires => 5.years.from_now }
u
end
end
I will accept another answer if you can show me a better way to do this.

The above solution works great.
Don't forget to setuphelper_method :current_or_guest_user to make the method accessible in views. Took me some time to figure out.

Related

Rails - Handle User roles using pundit

I have a table of users with enum user_type [Manager, Developer, QA]. Currently, I'm handling sign in using Devise and after login I'm using the following logic to display the appropriate webpage:
class HomeController < ApplicationController
before_action :authenticate_user!
def index
if current_user.manager?
redirect_to manager_path(current_user.id)
end
if current_user.developer?
redirect_to developer_path(current_user.id)
end
if current_user.quality_assurance?
redirect_to qa_path(current_user.id)
end
end
end
I want to use pundit gem to handle this. From the documentation, it transpired that this logic will be delegated to policies but I can't figure out how. Can somebody help me in implementing pundit in my project?
This is my users table:
I have created a user_policy but its mostly empty:
class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
end
User model:
You want to use Pundit to authorize a user, as in check if that user should be allowed to visit a controller action. If the user is not authorized for a specific action it raises a Pundit::NotAuthorizedError
You can check if a user is allowed to perform an action in the pundit policy, in which you have access to record (the instance thats passed to authorize) and user. So assuming you have a Flat Model, where only the owner can edit the Flat you might do this:
# flats_policy.rb
def edit?
record.user == user
end
Now lets say you also want to allow admins to edit you might do this
# flats_policy.rb
def owner_or_admin?
record.user == user || user.admin # where admin is a boolean
end
def edit?
owner_or_admin?
end
and the controller:
# flats_controller.rb
def edit
#flat = Flat.find(params[:id])
authorize #flat
# other code here
end
Now the index action is the odd one out because you would essentially have to call authorize on each instance, so the way Pundit handles this is with the Scope:
# flats_policy.rb
class Scope < Scope
def resolve
scope.all
end
end
and a corresponding index action might look like:
def index
#flats = policy_scope(Flat) # note that we call the model here
end
So lets say a user can only see flats that he/she owns:
# flats_policy.rb
class Scope < Scope
def resolve
scope.where(user: user)
end
end
and if admins can see all flats:
# flats_policy.rb
class Scope < Scope
def resolve
if user.admin
scope.all
else
scope.where(user: user)
end
end
end
In any case if the user is not allowed to perform an action you can rescue from the error like so:
# application_controller
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(root_path)
end
I guess you could do some dirty redirecting here, as in send admins to an admins_root_path, users to a default_root_path and so on...
On a final note, since this post is already too long you can check a policy in the view like this:
<% if policy(restaurant).edit? %>
You can see me if you have edit rights
<% end %>

Devise force logout for some users from Rails App

Is it possible to force logout for SOME of the users through Devise.
In my setup, I am using rails 4.1.15 with Devise 1.5.4. Sessions are persisted in db and there is no direct mapping with user ids. Is there a Devise way to logout against some of the users NOT ALL.
I tried resetting password as a proxy which logs out immediately but not always.
user_obj.update_attributes(:password => "some_random_string")
I can propose you next solution.
Add a new column for admin that called force_logout:boolean
In any your controller add a new action to set force_logout to true. Ex.:
# in admins_controller.rb
def force_logout
admin = Admin.find(params[:id])
admin.update_column(:force_logout, true)
redirect_to :back
end
In application_controller.rb add before_action to logout user if force_logout is true
before_action :check_force_logout
def check_force_logout
if current_user && current_user.force_logout?
current_user.update_column(:force_logout, false)
sign_out(current_user)
end
end
Too you need reset force_logout column after admin will be signed in. Usually you can do it session_controller.rb in action create.
Time to exhume this one. The only way I was able to solve this issue was by accessing the sessions. It is slow but it works.
def sign_out_different_user
#user = User.find(params[:id])
sessions = ActiveRecord::SessionStore::Session.where("updated_at > ?", Time.now - 480.minutes).all
sessions.each do |s|
if s.data['user_email'] == #user.email
s.delete and return
end
end
end
I needed to change the session expiration on the user object for my purposes, so I added this into the method, as well. YMMV
#user.session_expires_at = Time.now
#user.save

How to exclude guest users from getting emailed Rails 4 Devise

Just set up my first mailer on Rails 4. I have a welcome email sent to new users as soon as they sign up (create) for a new account using devise.
I also have devise set up so that if a current_user is not found, a guest user will be created. Unfortunately, this is interfering with the mailer. Every time a guest account is created, the mailer will send an email to a non-existing email.
I am having trouble figuring out how to exclude the guests from the mailer.
user.rb:
after_create :send_welcome_mail
def send_welcome_mail
UserMailer.welcome_email(self).deliver
end
mailers/user_mailer.rb:
class UserMailer < ActionMailer::Base
default from: "example#gmail.com"
def welcome_email(user)
#user = user
mail(:to => user.email, :subject => "Welcome!")
end
end
application_controller.rb (guest creation):
def current_or_guest_user
if current_user
if session[:guest_user_id] && session[:guest_user_id] != current_user.id
logging_in
guest_user(with_retry = false).try(:destroy)
session[:guest_user_id] = nil
end
current_user
else
guest_user
end
end
# find guest_user object associated with the current session,
# creating one as needed
def guest_user(with_retry = true)
# Cache the value the first time it's gotten.
#cached_guest_user ||= User.find(session[:guest_user_id] ||= create_guest_user.id)
rescue ActiveRecord::RecordNotFound # if session[:guest_user_id] invalid
session[:guest_user_id] = nil
guest_user if with_retry
end
private
# called (once) when the user logs in, insert any code your application needs
# to hand off from guest_user to current_user.
def logging_in
# For example:
# guest_comments = guest_user.comments.all
# guest_comments.each do |comment|
# comment.user_id = current_user.id
# comment.save!
# end
end
def create_guest_user
u = User.create(:name => "guest", :email => "guest_#{Time.now.to_i}#{rand(100)}#example.com")
u.save!(:validate => false)
session[:guest_user_id] = u.id
u
end
I'm sure this is easy, but I am still new to rails and am a bit confused on the best way to go about this. Let me know if you need any other code.
def welcome_email(user)
# The following line is unnecessary. Normally, you do
# something like this when you want to make a variable
# available to a view
##user = user
# You can make the if more explicit by writing
# if user.id == nil, but if will return false in
# Ruby if a value doesn't exist (i.e. is nil)
if user.id
mail(:to => user.email, :subject => "Welcome!")
end
end

Rails: Devise Login as another user issues

In my application, users have the ability to invite others to be 'Contributors' on their account. These contributors once they have logged in, are redirected to a dashboard that shows each account they can log in as.
The below is a controller that is used to allow Contributors and School Admins to log into Athlete accounts. When the original user logs into an account the app checks for the session[:original_user_id] variable to display a message banner across the top of the screen with a link so that the admin can log back into their account - This is what I am having issues with trying to figure out how to log the original user back in.
SignInAsController:
class SignInAsController < ApplicationController
before_filter :authenticate_user!
include SchoolAdmin::Athletes
def create
session[:original_user_id] = if (current_user.school_admin? || current_user.athlete_contributor?)
current_user.id
else
nil
end
user = User.find(params[:id])
if current_user.can_manage?(user)
sign_out(User.find(current_user.id))
handle_request(athlete)
redirect_to user_root_path
else
redirect_to :back, notice: "You do not have access to that account"
end
end
private
def handle_request(athlete)
sign_in(:user, athlete, { bypass: true })
end
end
UserModel can_manage? method:
class User < ActiveRecord::Base
#other methods
def can_manage?(user)
if athlete_contributor?
managed_athletes.include?(user)
elsif school_admin?
active_subscription.athletes.include?(user)
end
false
end
end
This is what worked for me. It allows an Admin user to login as another User.
class AdminController < ApplicationController
before_filter :authenticate_user!
def become
return unless current_user.is_an_admin?
sign_in(:user, User.find(params[:id]))
redirect_to root_path
end
end
You can also do sign_in(:user, User.find(params[:id]), { :bypass => true }) if you don't want to update last_sign_in_at and current_sign_in when admin becomes the user.
Devise has sessions & registrations controllers, which allow you to create & override various methods for devise
Sessions
Considering the logging-in process is about creating a session, I'd look at implementing my own method to destroy the current session & create a new one with the admin stats, like this:
#config/routes.rb
devise_for :users, :controllers => { :sessions => "sessions" }
devise_scope :user do
post "admin", :to => "devise/sessions#admin_switch"
end
#app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
#Switch to "admin" mode
def admin_switch
sign_out #method to destroy current user's session
sign_in #create new session -- needs more work
end
end
This is meant to demonstrate how you'd achieve your goal. If you want me to add some more code, I certainly will amend my post! Specifically, the sign_in command won't work (needs specific data passed to it, and uses Warden)
You can use Switch User gem which can work with:
:devise, :authlogic, :clearance, :restful_authentication or :sorcery
https://github.com/code-and-effect/effective_website/blob/develop/app/controllers/users/impersonations_controller.rb
this functionality “impersonating another user”. an admin can go onto Admin::Users#index -> Find a user, and impersonate them. That is, sign into their account.need a button that posts to this action
https://github.com/code-and-effect/effective_website/blob/develop/app/controllers/admin/users_controller.rb#L11
def impersonate
#user = User.find(params[:id])
authorize! :impersonate, #user
# Impersonate
session[:impersonation_user_id] = current_user.id
expire_data_after_sign_in!
warden.session_serializer.store(#user, Devise::Mapping.find_scope!(#user))
redirect_to(root_path)
end
before_action :authenticate_user!
skip_authorization_check only: [:destroy]
def destroy
#user = User.find(session[:impersonation_user_id])
# Reset impersonation
session[:impersonation_user_id] = nil
expire_data_after_sign_in!
warden.session_serializer.store(#user, Devise::Mapping.find_scope!(#user))
redirect_to(admin_users_path)
end
end
Which uses devise, to set the session[:impersonation_user_id] so later you know you are impersonating another use, and then this warden.session_serializer.store which signs you in as a new user.
If you close the tab. Reopen the tab. You will still be impersonating that user.
Put a partial in the site (haml) to display an alert to the user on every page when they’re impersonating
.bg-warning.d-print-none.text-center
You are logged in as <strong>#{current_user}</strong>.
= link_to 'click here', impersonate_path, 'data-method': :delete
to return to your original account.
https://github.com/code-and-effect/effective_website/blob/develop/app/views/layouts/_impersonate.html.haml

devise overrule devise SessionController broke login

I have extended the devise controller for sessions. To add some extra functionality when a user logs in. Now upon login, IF no username or password are entered I get error :
SessionsController#create
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
I searched last week for up to 2 hours how to fix this anyone can help me out on this one? Would be highly appreciated
How to correctly add my custom functionality but still preserve the Devise create action?
class SessionsController < Devise::SessionsController
def create
#user = User.where(:id => current_user.id).first
#moderated = Asset.where(:attachable_id => current_user.id, :moderated => true).first
if #user.sign_in_count.to_i == 1
def after_sign_in_path_for(resource)
"/welcome/basics"
end
else
if #moderated.nil?
unless #user.has_photo?
def after_sign_in_path_for(resource)
"/home/no_photo"
end
end
else
def after_sign_in_path_for(resource)
"/home/moderated"
end
end
end
end
end
If your additional functionality consists of redirecting the user to a different page at his first login i would suggest defining after_sign_in_path_for in the application controller as suggested by the wiki.
class ApplicationController < ActionController::Base
private
def after_sign_in_path_for(resource)
if current_user.sign_in_count == 1
"/welcome/basics"
else
"/other/path"
end
end
end
Please note that this only works if User is the only resource that can sign into your application. Otherwise you would have to differentiate in this method as well via the resource parameter.

Resources