Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Devise - how to get user when redirecting with a custom Devise::FailureApp

I let users log in initially without confirming their email address - but after 7 days, if they haven't confirmed - I block access until they confirm their address.

(Note - this is achieved by setting config.allow_unconfirmed_access_for = 7.days in the Devise initialiser)

If they hit the 'grace' limit (e.g. they don't confirm and 7 days pass) then I want to:

  1. send them to a page which explains what is going on (I can do this part)
  2. automatically re-send the confirmation email

to do #2 I need to access the user to get the email address.

Devise obviously 'knows' who the user is - that's how it knows they have passed the confirmation expiry.

If the user has just tried to log in, then I can get this by looking in the params. However if the user already has a live login token in their session, then when they pass the magical week - they'll suddenly start being rejected by devise. How do I access the user in this case?

#based on
#https://github.com/plataformatec/devise/wiki/How-To:-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated
#https://stackoverflow.com/questions/9223555/devise-with-confirmable-redirect-user-to-a-custom-page-when-users-tries-to-sig

class CustomFailure < Devise::FailureApp
  def redirect_url
    if warden_message == :unconfirmed
      user = User.find_by_email(params.dig(:user,:email))
      user&.send_confirmation_instructions

      if user.nil?
        #if the user had a valid login session when they passed the grace period, they will end up here
        !! how do I get the user in this scenario !!
      end

      confirmation_required_info_path(params: {found: !user.nil?})

    elsif warden_message == :invalid
      new_user_session_path(user:{email: params.dig(:user,:email)})
    else
      super
    end
  end

  # You need to override respond to eliminate recall
  def respond
    if http_auth?
      http_auth
    else
      redirect
    end
  end
end

This achieves goal #1, but it only achieves goal #2 if if the failure is the result of new signup

is there a direct way to access the user when they have a live session, but have passed the expiry date?

(current_user is not available, env['warden'].user is nil)

thank you

Rails 5.0.6 devise 4.2

edit: Updating to clarify with an example scenario where I need help:

day 0: User signs up with email/password. I let them in without confirming their email. They have a 7-day grace period to confirm their email.

day 2: They log out

day 7 (morning): They log in again

day 7 (later in the day): They do some action. Their login token is still valid - devise recognises it, finds their user record and checks if they have confirmed their email address. They have not - so devise refuses to authorise the action, giving the error :unconfirmed

In this scenario - they come through to the failure app. I will redirect them to a page which says 'you have passed your 7-day grace period, you really need to confirm your email address now'.

In the failure app, I want to know what their email address is so that I can automatically re-send the confirmation email. How do I get this?

Note - in this scenario, devise has refused authorisation. current_user is nil. However Devise clearly 'knows' who the user is - because it was able to look up their record in the database, and check that they had gone past the grace period for unconfirmed email addresses. How do I access that same 'knowledge'

like image 464
Confused Vorlon Avatar asked Jan 08 '18 16:01

Confused Vorlon


1 Answers

I think there are better ways of achieving the same result without creating a Devise::FailureApp:

This could be achieved by overriding the confirmed? method from Devise's resource extension present in the Confirmable module.

A simple example would be:

  1. Add a delayed_confirmation_expiry_date datetime field to your model's table, via migration.

This field will be used to store the expiry datetime when the user first registers into your app. You will have to override the SessionsController#create method for that, so it can call the #delay_confirmation! method on your resource.

  1. Add inside your User model equivalent :
# Will update the field you have added with the new temporary expiration access datetime
def delay_confirmation!(expiry_datetime=7.days.from_now)
  self.delayed_confirmation_expiry_date = expiry_datetime
  self.save
end

# Override that will make sure that, once the user is confirmed, the delayed confirmation information is cleared
def confirm(args={})
  clear_delay_confirmation!
  super
end

# Self-explanatory
def clear_delay_confirmation!
  self.delayed_confirmation_expiry_date = nil
  self.save
end

# Used on your controllers to show messages to the user warning him about the presence of the confirmation delay
def confirmation_is_delayed?
  self.confirmed? && !self.confirmed_at.present?
end

# Overrides the default implementation to allow temporary access for users who haven't confirmed their accounts within the time limit
def confirmed?
  if !self.confirmation_is_delayed?
    super
  else
    self.delayed_confirmation_expiry_date >= DateTime.now.in_time_zone
  end
end
like image 124
Rudy Seidinger Avatar answered Nov 03 '22 00:11

Rudy Seidinger