Authentication from scratch: "undefined method `find_by_username`" - ruby-on-rails

I'm trying to include a simple user authentication into my application, based on a filemaker database (using the ginjo-rfm gem). After getting some ideas from Ryan Bates' Authentication from Scratch, I've written a customized version of it, but running into some problems.
When I submit my login form, I'm presented with
undefined method `find_by_username' for User:Class
The find_by_username method should be based on a column in the database called 'username', is it not?
User.rb
class User < Rfm::Base
include ActiveModel::SecurePassword
include ActiveModel::MassAssignmentSecurity
include ActiveModel::SecurePassword
has_secure_password
attr_accessible :username, :password
config :layout => 'web__SupplierContacts'
def self.authenticate(username, password)
user = find_by_username(username)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
end
sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.authenticate(params[:username], params[:password])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_url, notice: "Logged in!"
else
flash.now.alert = "Email or password is invalid"
render "new"
end
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: "Logged out!"
end
end
I'm guessing this is a problem with my model inheriting from Rfm::Base, but I'm not sure. Any ideas?
Idea:
Is there any way to rephrase the Class.find_by_column statement? I'm not able to do User.where(:username => "username usernamerson", either (returns undefined method 'where' for User:Class).

If Rfm::Base does not extend ActiveRecord, then you won't be able to use the activerecord db query methods like find, where, etc. -- they are part of the ActiveRecord class and only available to classes which inherit from it.
If you want to include database wrapper methods in a class which extends another class (in this case Rfm::Base), you might have a look at DataMapper, which takes the form of a module (and thus can be included in any class). (DataMapper can be used as a replacement for ActiveRecord in Rails apps.)
Also, you've included ActiveModel::SecurePassword twice:
class User < Rfm::Base
include ActiveModel::SecurePassword
include ActiveModel::MassAssignmentSecurity
include ActiveModel::SecurePassword
I'd delete one of those.

Related

Skip 'has_secure_password' validations based on a condition

I have the following model:
class User < ActiveRecord::Base
has_secure_password
# ...
end
I'm trying to skip validations from has_secure_password helper based on a condition.
So, after searching I found a way in this answer to ALWAYS skip validations, however when I tried to adopt this solution in my case (as I said, I want to skip it based on a condition), as follows:
class User < ActiveRecord::Base
has_secure_password validations: :super_admin?
private
def super_admin?
p "role #{role.inspect}"
role == 'super_admin'
end
end
... it doesn't skip the validation. It doesn't even call the super_admin? method.
Thanks in advance.
for your case, the one that you need to skip is user.authenticate when you creating session for the user
below is sample of creating sesion with logical or if user.super_admin? equal true then it will pass and user can login
class SessionsController < ApplicationController
def create
user = User.find_by_username(params[:session][:username])
if user && (user.super_admin? || user.authenticate(params[:session][:password]))
login user
flash[:success] = "Successfully login"
redirect_to users_path
else
flash.now[:error] = 'sorry cannot login'
render 'new'
end
end
end
in user.rb, remove private and check as follow
class User < ActiveRecord::Base
def super_admin?
p "role #{role.inspect}"
role == 'super_admin'
end
end

Rspec test to check user gets a self-populated ip_location attribute filled (Rails 4/devise/geocoder)

In my devise sign-up page, i have implemented an ip tracking feature that, when a user signs up, sends the country the user comes from, in order to populate the newly created account's attribute 'user_country'
It works but as a newbie I don't know how to test with rspec that this works i.e that if i create a user who signs up, then AFTER account creation, that user_country is not empty any more.
Here how i use a /app/controllers/registrations_controller.rb method to assign a country to the user's newly created accoun on controllers/registrations_controller.rb- see below
class RegistrationsController < Devise::RegistrationsController
layout 'lightbox'
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
# required for settings form to submit when password is left blank
if account_update_params[:password].blank?
account_update_params.delete("password")
account_update_params.delete("password_confirmation")
end
#user = User.find(current_user.id)
if #user.update(account_update_params) # Rails 4 .update introduced with same effect as .update_attributes
set_flash_message :notice, :updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to after_update_path_for(#user)
else
render "edit"
end
end
# for Rails 4 Strong Parameters
def resource_params
params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :user_country)
end
private :resource_params
protected
def after_sign_up_path_for(resource)
resource.update(user_country: set_location_by_ip_lookup.country) #use concerns/CountrySetter loaded by ApplicationController
root_path
end
end
If it helps, this is how I find the country of a visitor:
module CountrySetter
extend ActiveSupport::Concern
included do
before_filter :set_location_by_ip_lookup
end
# we use geocoder gem
# output: ip address
def set_location_by_ip_lookup
if Rails.env.development? or Rails.env.test?
Geocoder.search(request.remote_ip).first
else
request.location
end
end
end
you need to use doubles in your test.
describe UserController do
it 'sets the request location with Geocoder' do
geocoder = class_double("Geocoder") # class double
location = double('location', country: 'some country') # basic double with return values
expect(geocoder).to receive(:search).and_return(location) # stubbing return value
post :create, user_params
user = assigns(:user)
expect(user.user_country).to eq('some country')
end
end
this way, we create a fake location and geocoder, and also create fake return values. then we assert that the return value from geocoder gets persisted as a user attribute. this way, we don't actually need to test whether Geocoder is working, and simplifies our test setup a lot.
the concepts used here:
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/basics/test-doubles
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/verifying-doubles/using-a-class-double
https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/configuring-responses/returning-a-value
if you want to be more robust, you might be able to fake ip address in the test request context, then assert what arguments the fake geocoder is receiving with argument matching: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
in general, you should utilize mocks to isolate your tests' concerns. see https://relishapp.com/rspec/rspec-mocks/v/3-0/docs for more documentation

Custom validation using helpers method

I want to make custom validation for Comment model: unregistered users shouldn't use e-mails of registered users, when they submitting comments.
I put custom validator class app/validators/validate_comment_email.rb:
class ValidateCommentEmail < ActiveModel::Validator
def validate(record)
user_emails = User.pluck(:email)
if current_user.nil? && user_emails.include?(record.comment_author_email)
record.errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end
end
And in my model file app/models/comment.rb:
class Comment < ActiveRecord::Base
include ActiveModel::Validations
validates_with ValidateCommentEmail
...
end
The problem is that I use current_user method from my sessions_helper.rb:
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
Validator can't see this method. I can include sessions_helper in Validator class, but it gives me an error about cookies method. It's a road to nowhere.
So how to make this custom validation rails way?
If the comment knows if it was created by a registered user (belongs_to :user), you can simply check against that:
def validate(record)
if record.user_id.nil? && User.where(:email => record.comment_author_email).exists?
record.errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end
If not, I think this validation should not be performed using a standard validator. It won't be aware of enough of the context to determine if the model meets this criteria. Instead, you should manually check this by passing the current_user from the controller itself:
# in comments_controller.rb
def create
#comment = Comment.new(params[:comment])
if #comment.validate_email(current_user) && #comment.save
...
end
# in comment.rb
def validate_email(current_user)
if current_user.nil? && User.where(:email => record.comment_author_email).exists?
errors[:comment_author_email] << 'This e-mail is used by existing user.'
end
end

create a count method for user logins with ruby on rails

what I want: A user logs in to his account he and automatically updates his own counter (#counter += 1).
I am new to Ruby and Rails and I am using Rails 3.2.12. I read the book "eloquent ruby", searched stackoverflow regarding this question and watched a video-ruby-course from pragmaticstudio.com. In that video-course they created a Class like this:
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation, :counter
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
def initialize(counter=0)
#counter = counter
end
def w00t
#counter += 15
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
end
Now in my App the User log-in is settled with a SessionsController and here come my problems because every method from the User model is "unknown" to the SessionsController.
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_back_or templates_path
else
flash.now[:error] = 'something went wrong.'
render 'new'
end
end
def destroy
sign_out
redirect_to root_url
end
end
Here is what I already tried but didn't work for my solution: i added
user.w00t
in the SessionsController, 1 line above
sign_in user
the returned error was: "undefined methode 'w00t' for SessionsController".
I also tried to write a method in the Sessions Helper:
def woot(template)
template.counter += 1
end
then I re-ordered my SessionsController 'create' method like so:
def create
template = Template.find_by_bosskey(params[:bession][:bosskey])
if template
woot template #that is my new line !
tsign_in template
redirect_back_or template
else
flash.now[:error] = 'something went wrong.'
render 'new'
end
end
With this i did not get any errors BUT still the counter doesnt change. I am more confused then ever. Please tell me WHERE to put that method or how to fix this problem for my app I am lost.
Your counter isn't being incremented because it's not being persisted to the database. Your using an instance variable which is only valid for the current request. As soon as you redirect and reload the page, that object is lost to the garbage collector, along with your counter.
To make the counter persistent you need to create a new column on user to hold the counter, then you can use the increment methods that Rails provides.
# create the migration
rails g migration add_sign_in_count_to_users sign_in_count:integer
rake db:migrate
# Then increment
class User < ActiveRecord::Base
def w00t
increment! :sign_in_count
end
end
ActiveRecord::Persistence#increment!

NameError when bypassing mass assignment while updating a user?

I followed the Railscast tutorial for bypassing mass assignment to edit my role attribute of my User model as the "admin". This is how I defined my roles:
class User < ActiveRecord::Base
attr_accessible :email, :password, :remember_me
attr_accessor :accessible
devise :database_authenticatable, ....etc
before_create :setup_default_role_for_new_users
ROLES = %w[admin default banned]
private
def setup_default_role_for_new_users
if self.role.blank?
self.role = "default"
end
end
def mass_assignment_authorizer
super + (accessible || [])
end
end
And then I created a new UsersController only to have issues with my update method:
def update
#user = User.find(params[:id])
#user.accessible = [:role] if user.role == "admin"
if #user.update_attributes(params[:user])
redirect_to #user, :notice => "Successfully updated user."
else
render :action => 'edit'
end
end
I can't do this though becuase this line: if user.role == "admin", is causing issue, giving me the error:
NameError (undefined local variable or method `user' for UsersController
What am I missing here?
Thanks in advance.
With the user part in user.role == "admin" you're trying to use a local variable, which hasn't been defined in your update method. If user isn't declared as a helper method that's accessible in your controllers then ruby won't find it.
From your code I'm assuming that only an admin user can update the role of another user? Thus you're not using #user.role == "admin" but user.role == "admin"?
If so you have to provide a user object whether it's through a helper method (i.e. in your ApplicationHelper class) or fetch it before you try to use it in your update method, or with a before_* callback in your controller.
I hope it's clear what I meant.

Resources