I have an app that uses devise for login. I have a login form in a web app that authenticates a user to the database and simply returns the #user hash as a json string.
The goal is to get the users authenticate the user and retrieve their authentication_token for future use in the app to prevent the user from having to continually log in.
The problem is that I can't get authentication_token to be included in the returned json.
My user.rb Model
attr_accessible :authentication_token, :email, :password, :password_confirmation, :remember_me, :bio, :confirmed, :deposit, :email, :fri25, :mon21, :name, :paid, :picture, :sat26, :sun20, :sun27, :thur24, :tue22, :wed23
clearly includes the authentication_token symbol.
Then in the session controller I have a custom action called newApi which runs a simple authentication method and responds with the #user hash as json.
def newapi
#user = User.authenticate(params[:email],params[:password])
respond_to do |format|
format.json { render json: #user }
end
eend
Yet no matter what I do the authentication token is not included with the json response. Am I missing something obvious? Why is it not included?
Devise > 2.x seems to strip out attributes from xml and json responses. This is likely a security feature, but I'm not sure where it got introduced. The list of "protected attributes" is here:
https://github.com/plataformatec/devise/blob/master/lib/devise/models/authenticatable.rb
line 54:
BLACKLIST_FOR_SERIALIZATION = [:encrypted_password, :reset_password_token, :reset_password_sent_at,
:remember_created_at, :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip,
:last_sign_in_ip, :password_salt, :confirmation_token, :confirmed_at, :confirmation_sent_at,
:remember_token, :unconfirmed_email, :failed_attempts, :unlock_token, :locked_at, :authentication_token]
and the code that initializes it is on line 122-135
The comments say you can provide your own list of blacklisted attributes or append to the existing list by using :force_except and :except, respectively.
My hacky solution would be this:
def newapi
#user = User.authenticate(params[:email],params[:password])
respond_to do |format|
format.json { render json: #user.as_json(:force_except => Devise::Models::Authenticatable::BLACKLIST_FOR_SERIALIZATION.delete_if{|x| x == :authentication_token} }
end
end
But I'm thinking there would be a way to override this list somewhere more elegantly like in an initializer. Mine works, but is likely not the best answer.
Though this is old, all I did was use with its "methods" option
to_json(methods: :arbitrary_method)
since all attributes technically become accessible methods on the model. This seemed most hassle free :D
On this page: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
My workaround:
class User < ActiveRecord::Base
# include devise token_authenticatable and others
devise :token_authenticatable
# after devise finds our user (such as when logging in)
after_find :write_auth_token
# write our pseudo attribute to the model
def write_auth_token
write_attribute(:auth_token, self.authentication_token)
end
# make our pseudo attribute attr_accessible
attr_accessible :auth_token
end
I ended up solving the problem myself by using jbuilder.
Essentially you just remove the render json: #user call and use a jbuilder template.
in newapi.json.jbuilder
json.(
#user,
:authentication_token,
:email,
:password,
:password_confirmation,
:remember_me,
:bio,
:confirmed,
:deposit,
:email,
:fri25,
:mon21,
:name,
:paid,
:picture,
:sat26,
:sun20,
:sun27,
:thur24,
:tue22,
:wed23
)
Review their docs if you got this route as there are plenty of It does everything I needed and more and is the only way we do api returns now. If you've not used it before go check it out! it make API endpoints amazingly simple.
Nick Knudson's answer is also valid if you want to roll your own setup.
#Travis Todd's answer totally does the work, and yes, there might be a more elegant solution. In addition, if you want to building a private or temporary api for db migration or other similar purposes where you need 2 or more of these blacklist attributes, you might want to do something like:
def newapi
#devise_attrs = [:encrypted_password, :remember_created_at, :sign_in_count, :current_sign_in_at] #etc..
#user = User.authenticate(params[:email],params[:password]).includes(:identity)
respond_to do |format|
# I added a few other options and updated ruby syntax so that you can see how can this be customized if in need of specific user attributes or relations.
format.json { render json: #user.as_json(force_except: Devise::Models::Authenticatable::BLACKLIST_FOR_SERIALIZATION.delete_if{|x| #devise_attrs.include?(x)}, include: [:identity]), status: 200 }
end
end
Still waiting for another finger-licking elegant solution for this.
Related
First of all, I believe there must be some people, who already asked this question before but I don't know how can I google this problem. So, if it is duplicate I am sorry.
I am working on a social media site. I have user model, which I use to register users to the site. It validates, name, email, and password when registering.
I use the same model to make users edit their informations, like username.
This is what I have in my update controller:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if #profile.update_attributes!(settings_profile_params)
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private # user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(:first_name, :last_name, :username, :school, :program, :website, :information)
end
The problem is, I only want to update strong parameters that I defined there. But I am getting an exception because of password validation. I don't know why am I getting this exception. How can I set up system to update the values in strong parameter only.
Thank you.
You can achieve this by changing you password validation. You need to add a condition on password validation.
# Password
validates :password,
:presence => {:message => 'Password cannot be blank'},
:length => {:within => 8..99, :message => 'Password length should be within 8 and 99 characters'}
:if => Proc.new { new_record? || !password.nil? }
By calling update_attributes you are implicitly invoking the same range of validations as an other update and save. You need to update on the specific params you're targeting (e.g. omitting :password).
Here, we can store that list of permitted keys in a variable that is reusable. Then we call update_attribute on each of those keys — doing so within a reduce that gives the same true/false for the switch to edit or display.
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if PERMITTED_KEYS.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
PERMITTED_KEYS = [:first_name, :last_name, :username, :school, :program, :website, :information]
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(PERMITTED_KEYS)
end
Having not used strong_parameters gem before, I think this would be more idiomatic to the use of the gem:
def update
# Find an existing object using form parameters
#profile = User.find_by_id(current_user.id)
# Update the object
if settings_profile_params.keys.reduce(true) {|bool, key| bool &&= #profile.update_attribute(key, #profile.send(key)) }
# If save succeeds, redirect to itself
redirect_to request.referrer
else
# If save fails, redisplay the form so user can fix the problems
render('edit')
end
end
private
# user_params is not an action, that is why it is private.
def settings_profile_params
params.require(:user).permit(
:first_name, :last_name, :username,
:school, :program,
:website, :information
)
end
Though, I still think this is a duplicate question, since it regard how to update model data without all of the defined validation. I've answered in case the update_attributes loop is felt to be a sufficiently unique solution to warrant non-duplication.
Okay, now I found the problem. First of all, #Muntasim figured out a way to solve this problem. But I actually don't need to use this solution, because there is another easy way to fix this.
In this situation, when I let users to update their profiles, rails should not validate my password or any other column in user model, if I don't ask it to. But why was it validating? Because I have validates :password in user model. Instead it has to be validates :digest_password. Because I am using bcrypt.
I don't know why :password was working fine when I register even though I used bcrypt.
I'm trying to do a custom validation in my model using valid? method. I need to run all validations of this model, except the password.
Something like this:
#resource = SalesPartner.new(permitted_params.merge(parent: current_sales_partner))
respond_to do |format|
if #resource.valid?(except: :password)
#resource.generate_authentication_token
SalesPartnerMailer.mail_new_sales_partner(#resource.email,
{ auth_token: #resource.authentication_token, id: #resource.id, level: #resource.level }
).deliver
flash[:success] = "#{#resource.level} criado com sucesso."
format.html { render "#{#path}/index" }
else
logger.log_and_alert_save_error(#resource, flash, "Ocorreu um erro ao salvar.")
format.html { render "#{#path}/new" }
end
end
Is that possible?
Thanks everyone!
Any way here is a way that you can do with the help of context. Say you have a User model and you want to validates some fields.
validates_presence_of :name,: email, on: :special_context
validates_presence_of :name,:email, :password
With the above 2 lines, you can now do like below
#user.valid?(:special_context)
The above will validates name and email fields. And if you write now,
#user.valid?
This will perform the presence validations on the name, email and password fields as you wrote.
Read valid?(context = nil) to understand the context.
Runs all the validations within the specified context. If the argument is false (default is nil), the context is set to :create if new_record? is true, and to :update if it is not.
Validations with no :on option will run no matter the context. Validations with some :on option will only run in the specified context.
Check this documentation also as an official example.
Context could really help you, but following maybe not.
validates_presence_of :name,: email, on: :special_context
validates_presence_of :name,:email, :password
This is because when you use validates_presence_of without specifying its context, it will be applied to all context(nil). Please see ref here So, maybe you could try as follow code:
validates_presence_of :name, :email, on: :special_context
validates_presence_of :name, :email, :password, on: [ :create, :update]
Just found one more mechanism to skip some validation on specified context, because specify all other context is not always suitable:
validates_presence_of :name, :email, on: :special_context
validates_presence_of :name, :email, :password, if: -> {Array(validation_context).exclude?(:special_context)}
note: validation_context can be array.
Well I decided to try a different approach. Right now I have pages that are only accessible by number ids instead of usernames or emails. I would like it to be accessible by email addreses instead. I tried to override them but it failed in rails 4.0 and I tried the find_by_email command which also failed. This is the error message that I get: ActiveRecord::RecordNotFound in AccountsController#show.
The only method that did work is find(params[:id]) which only works for accounts that have id's attached them and fails completly if it is null.
Is there any other method to solve this?
Rails Controller
def show
#puts "****************************************"
#puts params
if #account.nil?
render "shared/404"
else
#if !current_account.nil?
respond_with #account
#else
# render "shared/403"
#end
end
def load_findaccount
#params[:id] remains fixed but find_by_id changes to username
#account = Account.find(params[:id])
#user_path(user)
Account.rb
class Account < ActiveRecord::Base
#def to_param # overridden
# email
#end
validates :first_name, :presence => true
validates :last_name, :presence => true
validates :email, :presence => true, :uniqueness =>{:case_sensitive => false}
end
You can use this Account.where(:email => "example#pop.com"). This will retrieve the records for that email id which I think will be at max one record in your code as email id is unique in your model.
I decided to resolve this on my own and use just Rails 3.2. So this will end it for this issue.
I have a Rails app with a user model that contains an admin attribute. It's locked down using attr_accessible. My model looks like this:
attr_accessible :name, :email, :other_email, :plant_id, :password, :password_confirmation
attr_accessible :name, :email, :other_email, :plant_id, :password, :password_confirmation, :admin, :as => :admin
And here's what my update method in my users controller looks like:
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user], :as => current_user_role.to_sym)
flash[:notice] = "Profile updated"
redirect_to edit_user_url(#user)
else
render 'edit'
end
end
I have a helper method in my application controller that passes back the role as a string:
def current_user_role
#current_user_role ||= current_user.admin? ? "admin" : "default"
end
helper_method :current_user_role
I've also set config.active_record.whitelist_attributes = true in config/application.rb.
I've verified that the current_user_role method is returning the proper value based on the current user's admin status. Rails isn't throwing a mass-assignment error. But when I try to update a user's admin status while logged in as an admin, Rails performs the update and silently ignores the admin attribute. Pulling up the user's record in the Rails console shows that the record hasn't been modified.
I have a feeling there's a Ruby- or Rails-specific issue at play that I'm not aware of. I can't locate any info on making the role dynamic. The best I could find was this.
There was an errant attr_accessor :admin in my model that was left in from a prior attempt at getting this to work. I overlooked it. Removing it fixed it.
So, the upshot is that this is a pretty simple way to get dynamic roles working in Rails 3.2.
Looks like it could be a bug in Rails 3.2
https://github.com/stffn/declarative_authorization/issues/127
I have an update form in Rails 3 for admin users that fails silently, despite having validations. It was working previously, but when I moved everything to a namespace, it no longer saves.
Here is the relevant code from my controller:
def update
#admin = Admin::Admin.find(params[:id])
respond_to do |format|
if #admin.update_attributes(params[:admin])
flash[:success] = "'#{#admin.name}' was successfully updated."
format.html { redirect_to admin_admins_path }
else
format.html { render action: "edit" }
end
end
end
And the model (unfinished, but previously working):
class Admin::Admin < ActiveRecord::Base
validates :name, :presence=>{:message=>"Name can't be blank"}
validates :email, :presence=>{:message=>"Email can't be blank"},
:length => {:minimum => 3, :maximum => 254, :message=>"Email must be between 3 and 254 characters"},
:uniqueness=>{:message=>"Email has already been registered"},
:format=>{:with=>/^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message=>"Email must be a valid email format"}
validates :password, :presence=>{:message=>"Password can't be blank"}
end
And the first part of the form partial:
<%= form_for(#admin) do |f| %>
Everything loads properly, but when I try to save, my validations are ignored and it redirects to the index page with a success message, but without saving the data. I have a feeling I'm missing something to do with namespaces, but I'm not completely sure what the problem is. Could it be looking for the model in the base model directory?
Did you inspect the params? I could imagine that params[:admin] does not contain the forms values anymore.
So, VirtuosiMedia and I stepped through it, and RoR adds an "admin_" to represent the Admin:: namespace, so we had to look for params[:admin_admin].