conditional validation in rails redirect - ruby-on-rails

Whats the best way to implement a zip code validation feature. I'm not talking about the format of the zip code, but rather validating that a user entered zip code is one that you do business in. example here: https://doughbies.co/
For example: I only deliver to zip code 12345 so if user puts in a different zip code he gets a fail message saying "we do not deliver to your area" but if the user enters 12345 he is redirected to the shop.
I'm thinking about generating a zip code model with the acceptable zip codes as a constants in an array. Then creating a deliverable? function that matches user input to one of the zip codes in the array constant. Just don't know what methods or validations I can use for this.

Do you have a model to represent the order? If so you could have the validation there, without the need of a separate model.
class Order < ActiveRecord::Base
SHIPPABLE_ZIPS = ['12345']
validate :zip_shippable
def zip_shippable
errors.add(:zip, "cannot be shipped to") unless SHIPPABLE_ZIPS.include?(zip)
end
end
And for how to use this in the controller, take creating an order as example:
class OrdersController < ActionController::Base
def create
#order = Order.new(order_params) # "order_params" is params from the form
if #order.save
redirect orders_path # redirect the user to another page
else
render :new # render the form again, this time #order would contain the error message on zip code
end
end
end

Related

How can I call a controller action from ActiveAdmin?

I have this method in my reports_controller.rb, which allows an user to send a status.
def send_status
date = Date.today
reports = current_user.reports.for_date(date)
ReportMailer.status_email(current_user, reports, date).deliver
head :ok
rescue => e
head :bad_request
end
How can I call this action from ActiveAdmin, in order to check if a User sent this report or not? I want it like a status_tag on a column or something.
Should I do a member action?
Thanks!
I'll address the issue of checking if a report has been sent later, but first I'll cover the question of how to call the controller action from ActiveAdmin.
While you can call ReportsController#send_status by creating an ActionController::Base::ReportsController and then calling the desired method, e.g.
ActionController::Base::ReportsController.new.send_status
this isn't a good idea. You probably should refactor this to address a couple potential issues.
app/controllers/reports_controller.rb:
class ReportsController < ApplicationController
... # rest of controller methods
def send_status
if current_user # or whatever your conditional is
ReportMailer.status_email(current_user).deliver
response = :ok
else
response = :bad_request
end
head response
end
end
app/models/user.rb:
class User < ActiveRecord::Base
... # rest of user model
def reports_for_date(date)
reports.for_date(date)
end
end
app/mailers/reports_mailer.rb
class ReportsMailer < ActionMailer::Base
... # rest of mailer
def status_email(user)
#user = user
#date = Date.today
#reports = #user.reports_for_date(#date)
... # rest of method
end
end
This could obviously be refactored further, but provides a decent starting point.
An important thing to consider is that this controller action is not sending the email asynchronously, so in the interest of concurrency and user experience, you should strongly consider using a queuing system. DelayedJob would be an easy implementation with the example I've provided (look into the DelayedJob RailsCast).
As far as checking if the report has been sent, you could implement an ActionMailer Observer and register that observer:
This requires that the User model have a BOOLEAN column status_sent and that users have unique email address.
lib/status_sent_mail_observer.rb:
class StatusSentMailObserver
self.delivered_email(message)
user = User.find_by_email(message.to)
user.update_attribute(:status_sent, true)
end
end
config/intializer/setup_mail.rb:
... # rest of initializer
Mail.register_observer(StatusSentMailObserver)
If you are using DelayedJob (or almost any other queuing system) you could implement a callback method to be called on job completion (i.e. sending the status email) that updates a column on the user.
If you want to track the status message for every day, you should consider creating a Status model that belongs to the User. The status model could be created every time the user sends the email, allowing you to check if the email has been sent simply by checking if a status record exists. This strategy is one I would seriously consider adopting over just a simple status_sent column.
tl;dr ActionController::Base::ReportsController.new.send_status & implement an observer that updates a column on the user that tracks the status. But you really don't want to do that. Look into refactoring like I've mentioned above.

Ruby on Rails security vulnerability with user enumeration via id

With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.

Rails: How to enter value A in Model X only if value A exists in Model Y?

I'm trying to build a registration module where user can only register if their e-mail is already in an existing database.
Models:
User
OldUser
The condition on User will be
if OldUser.find_by_email(params[:UserName]) exists, allow user registration.
If not, then indicate error message.
This is really simple to do in PHP where I can just run a function to execute a mysql query. However, I couldn't figure out how to do it on Rails. It looks like I have to create a custom validator function but seems to be overkilled for a such simple condition.
It should be pretty simple to do. What have I missed?
Any pointer?
Edit 1:
This solution by dku.rajkumar works with a slight modification:
validate :check_email_existence
def check_email_existence
errors.add(:base, "Your email does not exist in our database") if OldUser.find_by_email(self.UserName).nil?
end
For cases like this, is it better to do validation in the model or at the controller?
you can do it as
if OldUser.find_by_email(params[:UserName])
User.create(params) // something like this i guess
else
flash[:error] = "Your email id does not exist in our database."
redirect_to appropriate_url
end
UPDATE: validation in model, so the validation will be done while calling User.create
class User < ActiveRecord::Base
validates :check_mail_id_presence
// other code
// other code
private
def check_mail_id_presence
errors.add("Your email id does not exist in our database.") if OldUser.find_by_email(self.UserName).nil?
end
end
I'd recommend starting with Devise.
See https://github.com/plataformatec/devise
Even if you have unusual needs like these, you can normally adapt it. Once you get to know it, it's extremely powerful, solid and debugged, and you can do all sorts of things with it.
Bellow is just an initial implementation .../app/controller/UsersController for User registration related actions.
def new
#user = User.new
end
def create
#user = User.new(params[:user])
#old_user = User.find_by_email(user.email)
if #old_user
if #user.save
# Handle successful save
else
render 'new' # and render some error message telling why registration was not succeed
end
else
# render some page with some sort of error message of 'new' new users
end
end
Update:
Check out the following resources for more info:
Ruby on Rails Tutorial
Rails: User/Password Authentication from Scratch, Part I/II

How do I respect RESTful methods when using find_or_initialize_by in Rails 3.2?

I have a resource in my project that collects some information from a user. Basically it's a form that they fill out before they can access another area of the site. It then sets a cookie for a week, but if they come back it will look up their previous entry and keep their preferences tied to them (and will update any details as long as the email address matches).
Currently I have a Applicants controller that looks like this:
class ApplicantsController < ApplicationController
...
def create
#applicant = Applicant.find_or_initialize_by_email(params[:applicant])
if #applicant.new_record? ? #applicant.save : #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
def update
if #applicant.update_attributes(params[:applicant])
set_cookie_and_redirect
else
render 'new'
end
end
end
The set_cookie_and_redirect is a private method that just sets some cookies and redirects the user to a page. The code works, but it just feels dirty. It's essentially updating a record within the create method under the condition that it's not a new record. I'm also forced to have an update method in case an existing record comes back with a validation error--the form helper will then switch the form over to sending to the update method.
So to my point... is there a more appropriate way to push the update_attributes call in the create method to the update method? Or better put, is there a better way to respect the RESTful methods in isolating the create and update functionality?
UPDATE: I wanted to be a little more specific too. If the user has filled this form out before it will set a cookie so they don't have to fill it out again for seven days. However after seven days the cookie is expired and they see the form again. The controller doesn't know if the user is new or existing until they add user input into the form which is then compared based on the email address.
Thanks in advance! I definitely look forward to anyone's thoughts on this.
The create method should only create, and the update method should only update. Let Rails decide which is going to happen based on what is inside of #applicant when the form is rendered - It essentially does what you're doing: Checks if the record is new or not, and sends it to update/create accordingly. Example:
def applicant
#applicant = Applicant.find_or_initialize_by_email(cookies[:email])
# renders applicant.html.erb form
end
<%= form_for #applicant do |f| %>
# ... fields ...
<% end %>
def create
#applicant = Applicant.new(params[:applicant])
#applicant.save
# .. etc.
end
def update
#applicant = Applicant.find_by_email(cookies[:email])
#applicant.update_attributes(params[:applicant])
# ... etc.
end
Rails will send the request to the correct action based on the new_record? status of the Applicant object.

Processing data before saving to database

I have decimal field in my DB. Users can input values in two formats: with comma or point (11,11 or 11.11).
But MySQL allows to save data only in 'point' format, so i want to process data before saving with regex like this:
sub(/,/,".")
How can i do it in Rails3?
If I understand you correctly, this could be done in the controller or the model. I might use the before_save callback in the model to achieve this in the following way:
class Item < ActiveRecord::Base
before_save :standardise_numbers
...
protected
# Called before this object is saved to the DB
def standardise_numbers
self.number.sub!(",", ".")
end
end
Where number is the attribute you're wanting to convert.
I assume you don't need to convert it back to comma representation to display to the user? If you do, you may want to look into the internationalisation API for Rails, Il8n. It handles this kind of stuff and more, so definitely worth looking into.
Alternative Solution (edit)
Based on your feedback, my above solution doesn't work since the number is already converted and the decimal part lost when it is passed into the model. A similar piece of code could be used in the controller to intercept and convert the number in the params hash itself:
class PostController < ActionController
before_filter :standardise_numbers, :only => [ :create, :update ]
def create
#post = Post.create(params[:post])
end
protected
# Intercepts the params hash
def standardise_numbers
params[:post][:number].sub!(",", ".")
end
end
This simplifies the create and update methods, allowing you to deal with the hash in the same way you normally would.
I played this it and found this:
Suppose what in form field number, user inputs value '12,13'.
Value from form go to PostController to 'create' method
class PostController < ApplicationController
def create
#post = Post.new(params[:post])
#on this step instance of Post model created, validated and filled with relevant values
#so #post.number == '12' #(decimal), it cuts ',13'
#we need to redefine #post.number
#post.number = params[:post][:number].gsub(/,/,'.').to_f # => 12.13
#and after that save the post
#post.save
end

Resources