I currently use Devise for user registration/authentication in a Rails project. When a user wants to cancel their account, the user object is destroyed, which leaves my application in an undesired state.
What is the easiest way to implement a "soft delete", i.e. only removing personal data and marking the user as deleted? I still want to keep all record associations.
I assume I will have to first introduce a new "deleted" column for users. But then I am stuck with this default code in the user's profile view:
<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.</p>
Where can I find the :delete method? How should I overwrite the default Devise methods?
I could advise overriding destroy method on your User model to simply do update_attribute(:deleted_at, Time.current) (instead of actually destroying), but this deviation from standard API could become burdensome in the future, so here's how to modify the controller.
Devise has a bunch of default controllers out of the box. The best way to customize them is to create your own controller inheriting the corresponding devise controller. In this case we are talking about Devise::RegistrationsController — which is easily recognized by looking at source. So create a new controller.
class RegistrationsController < Devise::RegistrationsController
end
Now we have our own controller fully inheriting all the devise-provided logic. Next step is to tell devise to use it instead of the default one. In your routes you have devise_for line. It should be changed to include registrations controller.
devise_for :users, :controllers => { :registrations => 'registrations' }
This seems strange, but it makes sense because by default it's 'devise/registrations', not simply 'registrations'.
Next step is to override the destroy action in registrations controller. When you use registration_path(:user), :method => :delete — that's where it links. To destroy action of registrations controller.
Currently devise does the following.
def destroy
resource.destroy
set_flash_message :notice, :destroyed
sign_out_and_redirect(self.resource)
end
We can instead use this code. First let's add new method to User model.
class User < ActiveRecord::Base
def soft_delete
# assuming you have deleted_at column added already
update_attribute(:deleted_at, Time.current)
end
end
# Use this for Devise 2.1.0 and newer versions
class RegistrationsController < Devise::RegistrationsController
def destroy
resource.soft_delete
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed if is_navigational_format?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
end
# Use this for older Devise versions
class RegistrationsController < Devise::RegistrationsController
def destroy
resource.soft_delete
set_flash_message :notice, :destroyed
sign_out_and_redirect(resource)
end
end
Now you should be all set. Use scopes to filter out deleted users.
Adding onto hakunin's answer:
To prevent "soft deleted" users from signing in, override active_for_authentication? on your User model:
def active_for_authentication?
super && !deleted_at
end
You could use acts_as_paranoid for your User model, which sets a deleted_at instead of deleting the object.
A complete tutorial can be found at Soft Delete a Devise User Account on the Devise wiki page.
Summary:
1. Add a "deleted_at" DATETIME column
2. Override users/registrations#destroy in your routes
3. Override users/registrations#destroy in the registrations controller
4. Update user model with a soft_delete & check if user is active on authentication
5. Add a custom delete message
def devise_current_user
#current_user ||= warden.authenticate(scope: :user)
end
def current_user
if params[:user_id].blank?
devise_current_user
else
User.find(params[:user_id])
end
end
Related
I am developing an application using these three gem I wrote in the title of this post. I set up devise with the confirmable module(?), so when a user creates an account with its email/password, it receives a confirmation email. If the user sign up with facebook (using omniauth-facebook gem) devise skips the confirmation step.
In user.rb
"Of course the :confirmable is active in the model"
...
# Omniauth-facebook
def self.find_for_facebook_oauth(auth)
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.skip_confirmation!
# user.image = auth.info.image # assuming the user model has an image
end
end
...
The thing comes when I added the wicked gem for the wizard. I configured the routes file
in routes.rb
MyApp::Application.routes.draw do
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks",
:registrations => "registrations" }
root 'home#index'
# Registration wizard routes
resources :after_register
end
The I created a registration_controller to override the devise registration methods
class RegistrationsController < Devise::RegistrationsController
def create
super
end
protected
def after_sign_in_path_for(resource)
puts "<<<<<<<<<<<<<<<< SIGN IN"
after_register_path(:import_contacts)
end
def after_sign_up_path_for(resource)
puts "<<<<<<<<<<<<<<<< SIGN UP ACTIVE"
after_register_path(:import_contacts)
end
def after_inactive_sign_up_path_for(resource)
puts "<<<<<<<<<<<<<<<< SIGN IN INACTIVE"
after_register_path(:import_contacts)
end
end
And then, I created a new controller to handle the steps of the wizard with wicked.
class AfterRegisterController < ApplicationController
include Wicked::Wizard
before_filter :authenticate_user!
steps :import_contacts, :select_agents, :wish_form
def show
#user = current_user
render_wizard
end
def update
#user = current_user
#user.attributes = params[:user]
render_wizard #user
end
end
When I create a user rith email/password, the wizard comes and everything works fine, but when I try to sign up with facebook, the wizars never comes.
Any hint???
Thank you!!
If everything is configured in the usual way (and what I can see looks pretty standard), then signing in using Facebook won't go via the RegistrationsController at all. It will go via the OmniauthCallbacks controller which you haven't posted the code for. It depends what you do in there when they log in via Facebook. I assume that you have a facebook() method which calls User.find_for_facebook_oauth(auth). Are you then just signing them in rather than going to the RegistrationsController? If so, then the RegistrationsController won't be touched, so its overridden after_sign_in_path_for won't have any effect.
If you want to have your overridden after_sign_in_path_for take effect throughout your app, you can define it in your ApplicationController. If you only want it to take effect in your OmniauthCallbacksController, you could define it there. In either case, they'll be hitting it every time they log in using Facebook (and if you put it in the ApplicationController, every time anyone logs in using any method), so you'd need to keep track of the fact that they have already been through the wizard, assuming you want to make sure that it only happens the first time they sign in. If you're using the devise :trackable module, perhaps checking the user.sign_in_count would be appropriate, or perhaps you have some other way to easily check if they have been through the wizard already.
UPDATE FOR COMMENT QUESTIONS:
Your first question: "Assuming I put the after_sign_in_path_fot in ApplicationController, I should remove the after_inactive_sign_up_path_for method in RegistrationsController, right?" It depends on what behaviour you want. With the RegistrationsController as in your question it will go to the wizard after they've signed up and before they've confirmed their email (because of after_inactive_sign_up_path_for which will be called in this case). When they confirm and sign in, the after_sign_in_path_for in ApplicationController will send them to the wizard again. So yes, remove inactive from RegistrationsController if you just want wizard after sign in. Then probably after_sign_up_path_for(resource) in RegistrationsController is unnecessary because the default implementation in devise just calls after_sign_in_path_for(resource) which you will have in your ApplicationController. Anyway, after_sign_up_path_for() won't be called if you always require confirmation because of the logic in the default implementation of RegistrationsController.create() - requiring confirmation will result in resource.active_for_authentication? returning false which causes after_inactive_sign_up_path_for(resource) to be called.
For the question in your second comment, you said "If I remove the after_sign_in_path_for in ApplicationController" - I assume you meant RegistrationsController? If that's right, then yes, there would be no methods needed in your overridden version of RegistrationsController (if that's the whole controller you pasted in your question) because your create() just calls super, the after_sign_in_path_for would be in ApplicationController and you probably don't want either of the after_(inactive)_sign_up_path_for methods as discussed above. So yes, there would be no need for your RegistrationsController. You could remove it completely and remove the :registrations => "registrations" in routes.rb - the devise implementation of RegistrationsController will then be used again.
Then you say "just override the methods of RegistrationsController in ApplicationController?". The only method you will have left from your RegistrationsController is the after_sign_in_path_for(resource) in ApplicationController, so I don't think there will be any other methods from your RegistrationsController that you need in ApplicationController. Let me know if I've missed one of your requirements or made an incorrect assumption.
Is there anyone experience with Rails Devise plugin? Coz in my project, when the user typed username and password, I have to check in another table whether the user is active. There are two tables, first one is the user and the other one role_membership in role_membership table there's a column named active_status. I have to check whether the active_status = 1 otherwise the user cannot log in to the system. Anyone here knows how to configure the Devise plugin to check the value in another tables. I found some tutorials, but all of 'em mentioning about checking in a field in same table.
Thanks
modify your User model to include two additional methods
active_for_authentication?
inactive_message
see http://pivotallabs.com/users/carl/blog/articles/1619-standup-3-21-2011-deactivating-users-in-devise (NOTE: it is based on an older version of devise, below code should work)
class User
# check to see if a user is active or not and deny login if not
def active_for_authentication?
super && your_custom_logic
end
# flash message for the inactive users
def inactive_message
"Sorry, this account has been deactivated."
end
end
replace your_custom_logic with your specific code for determining if user is active or not
additional link: http://rubydoc.info/github/plataformatec/devise/master/Devise/Models/Authenticatable/
My best idea is to override devise session#create method.
In order to do it:
#app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(auth_options)
#resource.is_active? should be implemented by you
if resource.is_active?
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
else
#put here your inactive user response
end
end
end
and in routes.rb
devise_for :users, :controllers => {:sessions => "sessions" }
With the Confirmable module enabled, Devise will not allow an unconfirmed user to sign in after a predefined period of time has elapsed. Instead the user is redirected back to the sign in page with the flash message "You have to confirm your account before continuing".
This is an undesirable interaction model, as a flash notice does not provide adequate space to properly explain to the user why access has been denied, what "confirm your account" means, provide a link to resend the confirmation, and instructions on how to check your spam folder and so on.
Is there a way I can change this behaviour to redirect to a specific URL instead?
Sorry at first I thought you meant after Sign Up not Sign In. So the down below works for how to direct users after Sign Up and what you need to do for Sign In is to create a custom Devise::FailureApp
See the wiki page: https://github.com/plataformatec/devise/wiki/How-To:-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated
Then within your custom FailureApp overwrite redirect_url method from https://github.com/plataformatec/devise/blob/master/lib/devise/failure_app.rb:
def redirect_url
if warden_message == :unconfirmed
custom_redirect_path
else
super
end
end
For custom redirect after Sign Up:
There is a controller method after_inactive_sign_up_path_for within the RegistrationsController that you can overwrite to accomplish this.
First in your Routes you will need to specify to use your custom controller:
config/routes.rb:
devise_for :users, :controllers => { :registrations => "users/registrations" }
Second you create your custom controller that inherits from the normal controller in order to overwrite the method:
app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
protected
def after_inactive_sign_up_path_for(resource)
signed_up_path
end
end
In this case for my App my Devise model is User so you may want to change that namespace if your model is named differently. I wanted my users to be redirected to the signed_up_path, but you can change that to your desired path.
I just did this, but took a different approach.
in app/controllers/sessions_controller.rb:
class SessionsController < Devise::SessionsController
before_filter :check_user_confirmation, only: :create
#
# other code here not relevant to the example
#
private
def check_user_confirmation
user = User.find_by_email(params[:email])
redirect_to new_confirmation_path(:user) unless user && user.confirmed?
end
end
This worked for me and seemed minimally invasive. In my app new sessions always have to go through sessions#create and users always sign in with their email address, so this may be a simpler case than yours.
You can of course redirect_to any location you desire in the check_user_confirmation method. new_confirmation_path was the logical choice for me because it provides users with the resources to get confirmed.
This is my solution you need to add :unconfirmed message on devise locales below the sessions.
in app/controllers/sessions_controller.rb
def check_user_confirmation
user = User.where(email: params[:user][:email]).take
unless user && user.confirmed?
set_flash_message! :alert, :unconfirmed
expire_data_after_sign_in!
respond_with user, location: after_inactive_sign_up_path_for(user)
end
end
protected
def after_inactive_sign_up_path_for(resource)
new_user_session_path
end
I need to add some simple methods and actions when a new user registers through Devise.
I want to apply a notify method which will send an email to me.
I want to use acts_as_network to pass a session value and connect the new register to the person who invited them.
How do I customize, I looked at the docs, but I'm not entirely clear what I need to do....thanks!
This is what I'm doing to override the Devise Registrations controller. I needed to catch an exception that can potentially be thrown when registering a new User but you can apply the same technique to customize your registration logic.
app/controllers/devise/custom/registrations_controller.rb
class Devise::Custom::RegistrationsController < Devise::RegistrationsController
def new
super # no customization, simply call the devise implementation
end
def create
begin
super # this calls Devise::RegistrationsController#create
rescue MyApp::Error => e
e.errors.each { |error| resource.errors.add :base, error }
clean_up_passwords(resource)
respond_with_navigational(resource) { render_with_scope :new }
end
end
def update
super # no customization, simply call the devise implementation
end
protected
def after_sign_up_path_for(resource)
new_user_session_path
end
def after_inactive_sign_up_path_for(resource)
new_user_session_path
end
end
Note that I created a new devise/custom directory structure under app/controllers where I placed my customized version of the RegistrationsController. As a result you'll need to move your devise registrations views from app/views/devise/registrations to app/views/devise/custom/registrations.
Also note that overriding the devise Registrations controller allows you to customize a few other things such as where to redirect a user after a successful registration. This is done by overriding the after_sign_up_path_for and/or after_inactive_sign_up_path_for methods.
routes.rb
devise_for :users,
:controllers => { :registrations => "devise/custom/registrations" }
This post may offer additional information you might be interested in.
I have looked all over the place, and found a lot of info... but nothing works for me and I don't get it :(
I know that you are suppose to override the registration controller, like this:
class Users::RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(resource)
authors_waiting_path
end
end
Then following the example showed by Tony Amoyal http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/, I am supposed to change my routes to update the access the new controller:
devise_for :users, :controllers => { :registrations => "users/registrations" } do
#get '/author/sign_up', :to => 'devise/registrations#new'
#get '/client/sign_up', :to => 'devise/registrations#new'
get '/author/sign_up', :to => 'users/registrations#new'
get '/client/sign_up', :to => 'users/registrations#new'
end
Yes, I have something a bit strange here, because I am catching some specific path to send them to the registration page, this allows me to create effectively 2 registration scenario.
I commented what I had before I had overridden the registration controller.
Even with all this and my authors_waiting_path being a valid path, it just keeps on going to the sign-in page after registration :(
This is really frustrating.
Alex
edit: I also found this on the devise wiki: https://github.com/plataformatec/devise/wiki/How-To:-Redirect-after-registration-(sign-up)
But I have no idea where to define this create method ? should I override the session controller ???
edit 2:
I put a dummy override of the controller:
class Pouets::RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(resource)
authors_waiting_path
end
def new
super
end
def create
puts "was here"
super
end
def edit
super
end
def update
super
end
def destroy
super
end
def cancel
super
end
end
And I never the "was here" in my logs.... I really have the feeling that it's totally ignoring the override... I must be doing something wrong :(
Ok... I am able to override it so you should be either :0
Create folder app/controllers/users
put there registrations_controller.rb with: (option with session - but it will try sign_in and later redirect - it may be not intended behavior for you ). Furthermore this is from devise wiki and I am not sure if it works
class Users::RegistrationsController < Devise::RegistrationsController
def create
session["#{resource_name}_return_to"] = complete_path
super
end
end
restart application (just for ensure you don't trust anything)
All in all you must override Create If you want redirect only Users... if you want define some more complex scenario you should monkeypatch sign_in_and_redirect
so your controller will looks like
class Users::RegistrationsController < Devise::RegistrationsController
# POST /resource/sign_up
def create
build_resource
if resource.save
set_flash_message :notice, :signed_up
#sign_in_and_redirect(resource_name, resource)\
#this commented line is responsible for sign in and redirection
#change to something you want..
else
clean_up_passwords(resource)
render_with_scope :new
end
end
end
second option try to monkeypatch helper ....
module Devise
module Controllers
# Those helpers are convenience methods added to ApplicationController.
module Helpers
def sign_in_and_redirect(resource_or_scope, resource=nil, skip=false)
#intended behaviour for signups
end
end
end
end
I have tried the above solution and while it works, reading devise code, I have found that all you actually need in order to sign-out just registered user and redirect is:
to add is_approved or similar to your user table and
to add active_for_authentication? method in your User model
Code:
class User < ActiveRecord::Base
# ... some code
def active_for_authentication?
super && is_approved
end
end
Was a bit hard to find when I needed it, but that is all. I am actually writing it here in case someone else needs it.