Need to do ajax side-registration with devise. Created RegistrationsController:
class RegistrationsController < Devise::RegistrationsController
def create
if request.xhr?
build_resource(sign_up_params)
if resource.save
...
end
else
super
end
end
end
Noticed, that model does not validate confirmation of password, if I don't pass that field in XHR, or the field is empty, and Angular does not gather it before doing $http.post
Hacked it for a while in Angular, hardcoding initial $scope.reg = {'password_confirmation': ''} but wonder how do I fix this a normal way?
It's unlikely that your model's password confirmation is not validated once you've properly set up Devise. You'll know for sure if validation isn't being run if registration actually creates a new user. If it doesn't create a new user, then validation works fine.
The problem seems to lie in your create action. It looks like your if resource.save doesn't have an else block which is what is called when resource.save fails due to validation errors. Your create action should look like:
def create
# seems unnecessary to check if request is xhr
build_resource(sign_up_params)
if resource.save
render json: resource
else
render :json => { :errors => #model.errors.full_messages }, :status => 422
end
end
Then just have code in Angular that handles errors by notifying the user.
Related
I'm setting up a CRUD feature and I only want a few people (entire email addresses) to be able to register through devise.
I've gone through countless posts but they mostly use email domains or single person login. I've also tried creating my own validation method in my User.rb but I can't seem to get it.
validate :check_email
private
def check_email
#users = User.all
if #users == '123.example#gmail.com')
events_path
else
errors.add(:email, 'is not authorized')
end
end
I don't get an explicit error but the app seems to skip my 'if' condition and outputs the 'else' condition.
To allow only users with certain email addresses you could simply add an inclusion validation.
class User < ApplicationRecord
ALLOWED_EMAILS = %w[
123.example#gmail.com
456.example#gmail.com
789.example#gmail.com
].freeze
validates :email, inclusion: { in: ALLOWED_EMAILS, message: :invalid }
# ...
end
You could also opt to load the ALLOWED_EMAILS from the settings or a file.
To load from the Rails config you have to define the email addresses in a config file.
config.allowed_user_emails = %w[
123.example#gmail.com
456.example#gmail.com
789.example#gmail.com
]
Then load them in the controller using:
ALLOWED_EMAILS = Rails.configuration.allowed_user_emails.freeze
To load from for example a yaml file you could do something like:
ALLOWED_EMAILS = YAML.load_file(Rails.root.join('config', 'allowed_user_emails.yml')).freeze
Having the following in the file:
- 123.example#gmail.com
- 456.example#gmail.com
- 789.example#gmail.com
The easiest way to accomplish this would be to create a custom validation like you did originally. In your user model you could create a validation that looks something like this:
validate :is_email_valid?
def is_email_valid
if ["example#gmail.com", "example#yahoo.com", "123.example#gmail.com"].include?(self.email)
errors.add :base, "Your email is not authorized for use!"
end
end
This code will work when saving or creating a new record.
The problem with your code is that you are attempting to validate on a ActiveRecord_Relation object which cannot directly access validations for class instances. It's like trying to call an instance method from a class level; you have to validate one user at a time. You will want to execute validations on your object instance with a reference to self. So, looping through your users and then validating would work. Here is an example:
User.all.each do |i|
if i.valid?
puts "VALID"
else
puts "INVALID"
end
end
Sarah,
The first step that I'd take in accomplishing this would be to work inside devise's registrations_controller. This is the controller where the signed up user will be served to and here you will want to over-write some of the Devise code.
Ensure the controllers are created first by running Devise's controller generator:
rails generate devise:controllers users
Then you'll want to find the user's registrations_controller.rb and override the code in there under the create action.
Here's an example of some code I wrote to override Devise's admin controller:
def create
build_resource(sign_up_params)
# Below - If admin is coming from an email invite or shared invite link, grabs both token and workplace id from params.
#token = params[:invite_token] if params[:invite_token]
#workplace_id = params[:workplace_id] if params[:workplace_id]
#workplace = Workplace.find(params[:workplace_id]) if params[:workplace_id] # Finds the workplace from the workplace_id, works for both invite email and shared link.
#institute = #workplace.institute if params[:workplace_id]
if #institute && #institute.has_super_admins?
resource.super_admin = false
end
if resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
# Below - If admin came from a shared workpalce invite link or email workplace invite
if #token != nil # Admin signed up via a workplace invite email or the shared link
# Below - Checks Payment plan for admins joining an institute to make sure the institute didn't exceed user count subscription permission.
unless #institute.plan.unlimited_users?
if #institute.plan.user_count <= #institute.admins.count # Admin doesn't join workplace since institute has excited user allowance
set_flash_message! :alert, :signed_up_no_workplace, :workplace => #workplace.name, :institute => #institute.name, :workplace_owner => #institute.subscription.admin.name
sign_up(resource_name, resource)
respond_with resource, location: institute_admin_path(resource.institute, resource)
else # Admin successfully signs up and joins the workplace. Method is below in protected
join_workplace_and_redirect
end
else # Admin successfully signs up and joins the workplace. Method is below in protected
join_workplace_and_redirect
end
else # Fresh admin signup
sign_up(resource_name, resource)
if resource.super_admin? # Checks if the admin is a super_admin and set as true, if so redirects to another page
set_flash_message! :notice, :super_admin_signed_up, :name => resource.first_name
respond_with resource, location: new_institute_path()
else # Admin is not a super_admin and not first one who signed up inside a city.
set_flash_message! :notice, :admin_signed_up, :link => edit_institute_admin_path(resource.institute, resource), :city => resource.institute.name
respond_with resource, location: after_sign_up_path_for(resource)
end
end
else
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
respond_with resource, location: after_inactive_sign_up_path_for(resource)
end
else
flash[:alert] = "Your profile could not be created. See why below!"
set_flash_message! :alert, :"signed_up_but_#{resource.inactive_message}"
redirect_to request.referrer
end
else # Failed to save
clean_up_passwords resource
respond_with resource, location: new_admin_registration_path(workplace: #workplace)
end
end
To apply it to your case, what you may want to do is use a case statement to see if the email matches ones you want it to. For example, below you'll want to check the resource (the single user in this case, the user who is signing up) email attribute to determine whether it's a success or failure:
# Checks emails that are allowed
case resource.email
when "123.example#gmail.com"
if resource.save # Success
set_flash_message! :notice, :signup_succeed
respond_with resource, location: home_page(resource)
else # For some reason the allowed email didn't go through due to other validations
set_flash_message! :alert, :signup_failure
respond_with resource, location: new_user_path(resource)
end
else # Entered an email that is not allowed
set_flash_message! :alert, :email_invalid
respond_with resource, location: new_user_path(resource)
end
The set_flash_message! is Devises custom messages which can be edited in config/locales/devise.en.yaml. The second keyword refers to the name of the yaml key, and you can customize the error or success message in there. respond_with is the redirection and location. You can use as many when statements as needed. This is just one way to do it.
Hope this helps.
I'm trying to build a controller action which will create a new user in my application. I want it to attempt to create the user and if it succeeds, return the User model and if it fails show the validation errors. So far I've got this mostly up and running:
# app/controllers/v1/users_controller.rb
def create
#user = User.create(user_params)
end
private
def user_params
params.require(:user).permit(:email, :password) if params[:user]
end
and I'm using JBuilder in my view like so:
# app/views/v1/users/create.json.jbuilder
if #user.errors
json.errors #user.errors
else
json.user #user
end
This looks like it'd work to me, however when I create a new user that doesn't have any validation errors I get this output rather than the User that just got created:
{"errors":{}}
How come it's printing out the empty errors when the user has no validation errors?
In Ruby, only nil and false evaluate to false in the if statement. In your example above, #user.errors is an ActiveModel::Errors class and therefore evaluate to true. Try changing the code above to:
if #user.errors.present?
json.errors #user.errors
else
json.user #user
end
I am using has_secure_password with a rails 4.1.5 app. I wanted to decouple my login functionality from my SessionsController so I can reuse it to login any user from wherever I want in my app - for example logging in a user after registration, logging analytics events etc.
So I refactored my code into a LoginUser service object and I am happy with it.
The problem is that my controller still has some coupled logic after this refactoring. I am using a Form Object (via the reform gem) for form validation and then passing on the user, session and password to the LoginUser service.
Here is what the create method in my SessionsController looks like:
def create
login_form = Forms::LoginForm.new(User.new)
if login_form.validate(params[:user]) # validate the form
begin #find the user
user = User.find_by!(email: params[:user][:email])
rescue ActiveRecord::RecordNotFound => e
flash.now.alert = 'invalid user credentials'
render :new and return
end
else
flash.now.alert = login_form.errors.full_messages
render :new and return
end
user && login_service = LoginUser.new(user, session, params[:user][:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
Everything is working as expected but the part I am not happy with is the tied up logic between validating the form and then finding the user before sending it to the service object. Also the multiple flash alerts feel..well..not right.
How would I make this method better by decoupling these two? It seems right now that one is carrying the other on it's back.
For your reference here is my LoginUser service object
class LoginUser
include Wisper::Publisher
attr_reader :user, :password
attr_accessor :session
def initialize(user, session, password)
#user = user
#session = session
#password = password
end
def execute
if user.authenticate(password)
session[:user_id] = user.id
publish(:user_authenticated, user)
else
publish(:user_login_failed)
end
end
end
What sticks out to me the most here is that create is a method with multiple responsibilities that can/should be isolated.
The responsibilities I see are:
validate the form
find the user
return validation error messages
return unknown user error messages
create LoginService object, setup after-auth behavior and do auth
The design goal to clean this up would be to write methods with a single responsibility and to have dependencies injected where possible.
Ignoring the UserService object, my first shot at a refactor might look like this:
def create
validate_form(user_params); return if performed?
user = find_user_for_authentication(user_params); return if performed?
login_service = LoginUser.new(user, session, user_params[:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
private
def user_params
params[:user]
end
def validate_form(attrs)
login_form = Forms::LoginForm.new(User.new)
unless login_form.validate(attrs)
flash.now.alert = login_form.errors.full_messages
render :new
end
end
def find_user_for_authentication(attrs)
if (user = User.find_by_email(attrs[:email]))
user
else
flash.now.alert = 'invalid user credentials'
render :new
end
end
Of note, the return if performed? conditions will check if a render or redirect_to method has been called. If so, return is called and the create action is finished early to prevent double render/redirect errors.
I think this is a big improvement simply because the responsibilities have been divvied up into a few different methods. And these methods have their dependencies injected, for the most part, so that they can continue to evolve freely in the future as well.
Is it possible to update from an action/method other than the update action/method? For example in my users controller I already have an update method for other parts of my users account.
I need a separate one for changing my users password. Is it possible to have something like this:
def another_method_to_update
user = User.authenticate(current_user.email, params[:current_password])
if user.update_attributes(params[:user])
login user
format.js { render :js => "window.location = '#{settings_account_path}'" }
flash[:success] = "Password updated"
else
format.js { render :form_errors }
end
end
Then have my change password form know to use that method to perform the update?
It has 3 fields: current password new password confirm new password
and I use ajax to show the form errors.
Kind regards
Yes; the update action is just a default provided to make REST-based interfaces trivially easy. You will want to make sure you have a POST route in config/routes.rb that references users#another_method_to_update presuming you're doing all this in the UsersController (and on Rails 3) but the basic answer to your question is that model operations (including updating fields) can be done anywhere you have the model available.
There's no tying between what model methods can be called and what controller methods are being invoked.
Why would you want to use another route for this? Stick with the convention, use the default route. If I understood correctly, your page contains a form for password update.
We could write entire code for this in the update method, but this is cleaner and more self-explanatory:
def update
change_password and return if change_password?
# old code of your update method
# ...
end
private
def change_password?
!params[:current_password].nil?
end
def change_password
user = User.authenticate(current_user.email, params[:current_password])
respond_to do |format|
if user.update_attributes(params[:user])
login user
flash[:success] = "Password updated"
format.js { render :js => "window.location = '#{settings_account_path}'" }
else
format.js { render :form_errors }
end
end
end
This is a lot easier to understand for someone who will look at your code since you're still calling update method to update your model which then performs custom action.
I've also fixed your custom method code.
in config/routes.rb:
puts "users/other_update_method"
I currently use Devise for user registration/authentication in a Rails project.
When a user wants to cancel their account, the user object is soft deleted in a way like the following.
How to "soft delete" user with Devise
My implmenetation has a small difference this way.
User model has an attribute 'deleted_flag'.
And, soft_delete method executes "update_attribtue(:deleted_flag, true)"
But, I have to implment sign_in action.
In my implmenetation is the following.
class SessionsController < Devise::SessionsController
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
if resource.deleted_flag
p "deleted account : " + resource.deleted_flag.to_s
sign_out(resource)
render :controller => :users, :action => :index
else
if is_navigational_format?
if resource.sign_in_count == 1
set_flash_message(:notice, :signed_in_first_time)
else
set_flash_message(:notice, :signed_in)
end
end
sign_in(resource_name, resource)
respond_with resource, :location => redirect_location(resource_name, resource)
end
end
end
I think this code has strange points.
If deleted user tries to sing in,
the system permit logging and make log out immediately.
And, the system cann't display flash[:alert] message...
I want to know two points.
How do I implement to prohibit deleted users to login?
How do I implement to display flash[:alert] when deleted user tries to login?
To stop a user that has been 'soft deleted', the best way is to overwrite the find_for_authentication class method on the user model. Such as:
Class User < ActiveRecord::Base
def self.find_for_authentication(conditions)
super(conditions.merge(:deleted_flag => false))
end
This will generate a invalid email or password flash message by devise (because it cannot find the user to authenticate)
As far as your second question though, you'll need some for of method in your controller to add a particular flash message. However, in my opinion you should treat users that are 'soft' deleted the same as if they didn't exist in the database at all. Thus if they tried to log in, they should just get an valid email or password message.
See my solution here: https://stackoverflow.com/a/24365051/556388
Basically you need to override the active_for_authentication? method on the devise model (User).
I haven't tried anything like that but it seems if you want to catch the user before authentication you'll either have to write a Devise authentication strategy or a before_filter to be run before authenticate_user!. Something like:
before_filter :no_deleted_users
def no_deleted_users
if User.find(params[:email]).deleted?
redirect_to root_path, :flash => { :error => "Your user was deleted. You cannot log in." }
end
end
Although it might be more complex to get the user than that. I haven't played with Devise pre-authentication.
The modern and correct answer is this:
class User < ApplicationRecord
def active_for_authentication?
super && !discarded? # or whatever...
end
end
See the documentation here.