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
Related
I have an authentication feature that provides me the username of the current user. I also have a table of events (created by users). And when a user creates an event, how could I save a field called host with the current name of this user in the table events?
The concept is called "deserialization", which is why I made my own gem for this: quickery.
Using quickery
class Event < ApplicationRecord
belongs_to: user
quickery { user: { username: :host } }
end
Using Normal-Way
class Event < ApplicationRecord
belongs_to :user
before_save do
host = user.username
end
end
class User < ApplicationRecord
has_many :events
# you may want to comment out this `after_destroy`, if you don't want to cascade when deleted
after_destroy do
events.update_all(host: nil)
end
# you may want to comment out this `after_update`, if you don't want each `event.host` to be updated (automatically) whenever this `user.username` gets updated
after_update do
events.update_all(host: username)
end
end
Usage Example (for either above)
user = User.create!(username: 'foobar')
event = Event.create!(user: user)
puts event.host
# => 'foobar'
Or, if your Event doesn't belongs_to :user, then you'll need to update this manually in the controller as follows
class EventsController < ApplicationController
def create
#event = Event.new
#event.assign_attributes(event_params)
#event.host = current_user.username
if #event.save
# success, do something UPDATE THIS
else
# validation errors, do something UPDATE THIS
end
end
def update
#event = Event.find(params[:id])
#event.assign_attributes(event_params)
#event.host = current_user.username
if #event.save
# success, do something UPDATE THIS
else
# validation errors, do something UPDATE THIS
end
end
private
def event_params
params.require(:event).permit(:someattribute1, :someattribute2) # etc. UPDATE THIS
end
end
I have a user registration with an extra field called "company_name". After the user gets created, I want a Company instance to be created based on the extra field "company_name" and that user associated with the company. I've tried a few things like this:
class RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
super
company = Company.create(name: params[:company_name])
current_user.admin = true
current_user.company = company
current_user.save
end
def update
super
end
end
however, I don't have a current_user when trying to do the lines after I create the company. Is there a better way of doing this?
You can pass a block to the Devise controller's create that will give you the created user resource:
class RegistrationsController < Devise::RegistrationsController
CREATE_COMPANY_PARAMS = [:name]
def create
super do |created_user|
if created_user.id
company = Company.create! create_company_params
created_user.update! company_id: company.id
end
end
end
private
def create_company_params
params.require(:user).require(:company).permit(*CREATE_COMPANY_PARAMS)
end
end
There are some tough parts to handling this correctly though.
It seems that even if the user already exists, it will still call your block and pass you a user, but the user won't have an id assigned because the DB save failed. The if created_user.id check prevents a company from being created for an invalid user.
If the company already exists. The .create! will throw an exception which causes the controller to return an HTTP 422.
Utilizing the after_save callback in User model is probably suitable for this case:
# app/models/user.rb
class User < ActiveRecord::Base
...
# Execute this callback after an record is saved only on create
after_save :create_and_associate_company, on: :create
private:
def create_and_associate_company
company = self.companies.build
# Other necessary attributes assignments
company.save
end
end
Reference on other Active Record Callbacks.
You can access the newly created user using the resource variable
Here, I'm logging info only if the user was actually saved
class RegistrationsController < Devise::RegistrationsController
def create
super
if resource.persisted?
Rails.logger.info("Just created and saved #{resource}");
end
end
end
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
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.
Within Authlogic, is there a way that I can add conditions to the authentication method? I know by using the find_by_login_method I can specify another method to use, but when I use this I need to pass another parameter since the find_by_login_method method only passes the parameter that is deemed the 'login_field'.
What I need to do is check something that is an association of the authentic model.. Here is the method I want to use
# make sure that the user has access to the subdomain that they are
# attempting to login to, subdomains are company names
def self.find_by_email_and_company(email, company)
user = User.find_by_email(email)
companies = []
user.brands.each do |b|
companies << b.company.id
end
user && companies.include?(company)
end
But this fails due to the fact that only one parameter is sent to the find_by_email_and_company method.
The company is actually the subdomain, so in order to get it here I am just placing it in a hidden field in the form (only way I could think to get it to the model)
Is there a method I can override somehow..?
Using the answer below I came up with the following that worked:
User Model (User.rb)
def self.find_by_email_within_company(email)
# find the user
user = self.find_by_email(email)
# no need to continue if the email address is invalid
return false if user.nil?
# collect the subdomains the provided user has access to
company_subdomains = user.brands.map(&:company).map(&:subdomain)
# verify that the user has access to the current subdomain
company_subdomains.include?(Thread.current[:current_subdomain]) && user
end
Application Controller
before_filter :set_subdomain
private
def set_subdomain
# helper that retreives the current subdomain
get_company
Thread.current[:current_subdomain] = #company.subdomain
end
User Session Model (UserSession.rb)
find_by_login_method :find_by_email_within_company
I have read a few things about using Thread.current, and conflicting namespaces.. This is a great solution that worked for me but would love to hear any other suggestions before the bounty expires, otherwise, +100 to Jens Fahnenbruck :)
Authlogic provides API for dealing with sub domain based authentication.
class User < ActiveRecord::Base
has_many :brands
has_many :companies, :through => :brands
acts_as_authentic
end
class Brand < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :brands
has_many :users, :through => :brands
authenticates_many :user_sessions, :scope_cookies => true
end
Session controller:
class UserSessionsController < ApplicationController
def create
#company = Company.find(params[:user_session][:company])
#user_session = #company.user_sessions.new(params[:user_session])
if #user_session.save
else
end
end
end
On the other hand
Here is a way to solve the problem using your current approach(I would use the first approach):
Set custom data - to the key email of the hash used to create the UserSession object.
AuthLogic will pass this value to find_by_login method. In the find_by_login method access the needed values.
Assumption:
The sub domain id is set in a field called company in the form.
class UserSessionsController < ApplicationController
def create
attrs = params[:user_session].dup #make a copy
attrs[:email] = params[:user_session] # set custom data to :email key
#user_session = UserSession.new(attrs)
if #user_session.save
else
end
end
end
Model code
Your code for finding the user with the given email and subdomain can be simplified and optimized as follows:
class User < ActiveRecord::Base
def find_by_email params={}
# If invoked in the normal fashion then ..
return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)
User.first(:joins => [:brands => :company}],
:conditions => ["users.email = ? AND companies.id = ?",
params[:email], params[:company]])
end
end
Edit 1
Once the user is authenticated, system should provide access to authorized data.
If you maintain data for all the domains in the same table, then you have to scope the data by subdomain and authenticated user.
Lets say you have Post model with company_id and user_id columns. When a user logs in you want to show user's posts for the sub domain. This is one way to scope user's data for the subdomain:
Posts.find_by_company_id_and_user_id(current_company, current_user)
Posts.for_company_and_user(current_company, current_user) # named scope
If you do not scope the data, you will have potential security holes in your system.
In your lib folder add a file with the follwing content:
class Class
def thread_local_accessor name, options = {}
m = Module.new
m.module_eval do
class_variable_set :"###{name}", Hash.new {|h,k| h[k] = options[:default] }
end
m.module_eval %{
FINALIZER = lambda {|id| ###{name}.delete id }
def #{name}
###{name}[Thread.current.object_id]
end
def #{name}=(val)
ObjectSpace.define_finalizer Thread.current, FINALIZER unless ###{name}.has_key? Thread.current.object_id
###{name}[Thread.current.object_id] = val
end
}
class_eval do
include m
extend m
end
end
end
I found this here
Then add code in the controller like this:
class ApplicationController < ActionController
before_filter :set_subdomain
private
def set_subdomain
User.subdomain = request.subdomains[0]
end
end
And now you can do the following in your user model (assuming your company model has a method called subdomain:
class User < ActiveRecord::Base
thread_local_accessor :subdomain, :default => nil
def self.find_by_email_within_company(email)
self.find_by_email(email)
company_subdomains = user.brands.map(&:company).map(&:subdomain)
company_subdomains.include?(self.subdomain) && user
end
end
And FYI:
companies = user.brands.map(&:company).map(&:subdomain)
is the same as
companies = []
user.brands.each do |b|
companies << b.company.subdomain
end
With rails 3 you can use this workaround:
class UserSessionsController < ApplicationController
...
def create
#company = <# YourMethodToGetIt #>
session_hash = params[:user_session].dup
session_hash[:username] = { :login => params[:user_session][:username], :company => #company }
#user_session = UserSession.new(session_hash)
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default dashboard_url
else
#user_session.username = params[:user_session][:username]
render :action => :new
end
...
end
Then
class UserSession < Authlogic::Session::Base
find_by_login_method :find_by_custom_login
end
and
class User < ActiveRecord::Base
...
def self.find_by_custom_login(hash)
if hash.is_a? Hash
return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
find_by_email_and_company_id(hash[:login], hash[:company].id)
else
raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
end
end
...
end
Which is quite plain and "correct". I take me a lot of time to find out, but it works fine!