Rails 4: How can I decouple logic in this long controller method? - ruby-on-rails

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.

Related

Rails devise signup with JWE token payload

I need to create functionality where other microservice creates a link to my app with JWE token as a params in which is encrypted json user params e.g.:
json_payload = {
email: 'test#test.com',
external_id: '1234'
}.to_json
The flow should be:
user gets the url generated by different app with JWE token as params (e.g. http://localhost:3000/users/sign_up/?jwe_token=some_gigantic_string_123)
enter that url
under the hood Rails app creates new user based on encrypted params
after successful user creation redirect that user to the edit page
So as you see, the user shouldn't notice that there was an account creation but the first page it will see is the password edit.
Am I not doing some sort of antipaternity here with below code? Please take a look:
class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/sign_up
def new
return redirect_to(new_user_session_path) unless params[:jwe_token]
json_payload = JWE.encrypt(payload, rsa_key)
payload = JSON.parse json_payload
user = User.new(user_params)
if user.save
redirect_to generate_password_url(request.base_url, user)
else
redirect_to new_user_session_path, alert: 'Something went wrong'
end
end
private
def generate_password_url(base_url, user)
path = edit_password_path(user, reset_password_token: fetch_token(user))
"#{base_url}#{path}"
end
def fetch_token(user)
user.send(:set_reset_password_token)
end
end
I assume that if user creation is to be handled by a link I have to use new method. Am I not creating an antipattern here? Is there any other way to do so?

Devise Custom Session Controller not Persisting Flash Messages

I decided to customize(override) Devise's session controller to implement my own sign in logic. I've left the sessions#new controller alone and simply call super in it.
def new
super
end
However, I have removed the call to super in my custom sessions#create method so that I can send an HTTP request to a server for authentication. Subsequently, I either sign in the user or return the user to the sessions#new path to try their login again.
def create
#member = Member.find_by_username(params[:username])
response = authenticate_with_api
if response[:authenticated]
if #member
#member.update(response.[:member])
sign_in #member
else
#member.create(response.[:member])
sign_in #member
end
else
flash[:alert] = "Incorrect username or password"
flash[:error] = "Incorrect username or password"
flash[:notice] = "Incorrect username or password" #setting all the messages out of frustration!
redirect_to new_member_session_path
end
The logic flow is working correctly, however the flash messages are not displaying. I have set a flash message just before calling super in the new method to ensure that the view was setup properly:
def new
flash[:notice] = "Test flash message"
super
end
This works. I can see the flash message appear in the template. The only explanation I can come up with is that somehow, there are multiple redirects occurring. However, when I look in the logs, I can only see the redirect I specified. I have also tried using keep to persist the flash message through multiple requests, but even that does not work:
flash.keep[:notice] = "Test flash message"
redirect_to new_member_session_path #does not work
I also tried using now as a last ditch effort:
flash.now[:notice] = "Test flash message"
No dice. Lastly, I have tried placing the flash message inline with the redirect:
redirect_to new_member_session_path, flash: { :notice => "Test flash message" } #also does not work
I am at a loss. Not sure why the flash messages are being lost. I am running Rails 4.1.2 and Devise 3.2.4
EDIT:
I have decided this is not the best way to customize authentication. Instead, I've created a custom Warden strategy and told devise about it:
#config/initializers/devise.rb
config.warden do |manager|
manager.intercept_401 = false
manager.default_strategies(:scope => :member).unshift :member_login
end
I put all my logic in that file:
Warden::Strategies.add(:member_login) do
def member_params
ActionController::Parameters.new(#response).permit(:first_name, :last_name, :role, :username, :parkplace_user_id)
end
def valid?
params[:member] && ( params[:member][:username] || params[:member][:password] )
end
def authenticate!
member = Member.find_by_username(params[:member][:username])
if authenticate_with_ppd_api
if member
member.update(member_params)
success!(member)
else
member = Member.create(member_params)
success!(member)
end
else
fail!("Incorrect username or password")
end
end
def authenticate_with_api
#response = HTTParty.post('https://path.to.server/admin/login', body: { username: params[:member][:username], password: params[:member][:password] } )
#response = JSON.parse(#response.body).first
#response["authenticated"]
end
end
Flash messages are appearing now, but I have other problems with sign in. I created a new question for that. Thanks!

Devise, blank password_confirmation

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.

How can I update my method using an alternative update method in ruby on rails?

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"

Force validation of blank passwords in Authlogic

I'm adding a password reset feature to my Rails application that uses Authlogic. I was following the guide here: http://www.binarylogic.com/2008/11/16/tutorial-reset-passwords-with-authlogic/ and everything works as I'd like except for one thing: the password reset form accepts blank passwords and simply doesn't change them.
I've been searching around, and have learned that this is the intended default behavior because it allows you to make user edit forms that only change the user's password if they enter a new one, and ignore it otherwise. But in this case, I specifically want to enforce validation of the password like when a user initially registers. I've found two possible solutions for this problem but haven't been able to figure out how to implement either of them.
1) Someone asked this same question on Google Groups:
User model saves with blank password
Ben's response was to use #user.validate_password = true to force validation of the password. I tried this but I get an undefined method error: undefined method 'validate_password_field=' for #<User>.
2) There seems to be an Authlogic configuration option called ignore_blank_passwords. It is documented here:
Module: Authlogic::ActsAsAuthentic::Password::Config#ignore_blank_passwords
This looks like it would work, but my understanding is that this is a global configuration option that you use in your initial acts_as_authentic call in the User model, and I don't want to change it application-wide, as I do have a regular edit form for users where I want blank passwords to be ignored by default.
Anyone found a solution to this? I see validate_password= in the change log for Authlogic 1.4.1 and nothing about it having been removed since then. Am I simply using it incorrectly? Is there a way to use ignore_blank_passwords on a per-request basis?
This is kind of an old thread, but since it is unanswered I'll post this.
I've managed to do it a bit more cleanly than the other solutions, "helping" authlogic validations with my own.
I added this to user:
class User < ActiveRecord::Base
...
attr_writer :password_required
validates_presence_of :password, :if => :password_required?
def password_required?
#password_required
end
...
end
You can reduce it to two lines by making an attr_accessor and using :if => :password_required (no interrogation), but I prefer this other syntax with the interrogation sign.
Then your controller action can be done like this:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
#user.password_required = true
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
This will have a local effect; the rest of the application will not be affected (unless password_required is set to true in other places, that is).
I hope it helps.
This what I did.
class User < ActiveRecord::Base
attr_accessor :ignore_blank_passwords
# object level attribute overrides the config level
# attribute
def ignore_blank_passwords?
ignore_blank_passwords.nil? ? super : (ignore_blank_passwords == true)
end
end
Now in your controller, set the ignore_blank_passwords attribute to false.
user.ignore_blank_passwords = false
Here, you are working within the confines of AuthLogic. You don't have to change the validation logic.
User.ignore_blank_passwords = false
Use model, not object for setting this property.
def update_passwords
User.ignore_blank_passwords = false
if #user.update_attributes(params[:user])
...
end
User.ignore_blank_passwords = true
end
Maybe test the value of the parameter in the controller? (air code):
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.password.blank?
flash[:error] = "Password cannot be blank"
render :action => :edit
return
end
if #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
Apart from zetetic's solution you could do it this way:
def update
#user.password = params[:user][:password]
#user.password_confirmation = params[:user][: password_confirmation]
if #user.changed? && #user.save
flash[:notice] = "Password successfully updated"
redirect_to account_url
else
render :action => :edit
end
end
You're basically checking if authlogic changed the user record (which it doesn't if the password is empty). In the else block you can check if the password was blank and add an appropriate error message to the user record or display a flash message.

Resources