How can I use acts_as_commentable for two models - ruby-on-rails

I want to use acts_as_commentable for two models (blog and post). I generated the comment model and added all my fields.
I created a comments_controller and in the create action I have to find blogs and posts, so for that to work I am doing something like this :-
def create
if controller_name == "blogs"
#blog = Blog.find(params[:comment][:blog_id])
#blog_comment = #blog.comments.new(:comment => params[:comment][:comment], :user_id => current_user.id)
if #blog_comment.save
flash[:success] = "Thanks for commenting"
redirect_to :back or root_path
else
flash[:error] = "Comment can't be blank!"
redirect_to :back or root_path
end
end
if controller_name == "topics"
#post = Post.find(params[:comment][:post_id])
#post_comment = #post.comments.new(:comment => params[:comment][:comment], :user_id => current_user.id)
if #post_comment.save
flash[:success] = "Thanks for commenting"
redirect_to :back or root_path
else
flash[:error] = "Comment can't be blank!"
redirect_to :back or root_path
end
end
I know it is pretty ugly but I don't know how to go ahead with this one, can anyone help me out?

I encountered the similar problem on one of my apps.
Here is what I've done:
class CommentsController < ApplicationController
before_filter :get_commentable
def create
#comment = #commentable.comments.build(params[:comment])
if #comment.save
...
else
...
end
end
private
def get_commentable
params.each do |k, v|
return #commentable = $1.classify.constantize.find(v) if k =~ /(.+)_id$/
end
end
end

Related

How to protect against tampering with the URL in Rails

My Rails student planner application has a few issues regarding URL tampering. I believe they probably all share a similar solution but I'm having difficulty.
When viewing an assignment (students/:id/assignments/:id), changing the assignment’s ID in the URL to the ID of an assignment belonging to another student sometimes leads to a "no method error" in my assignments#show page, other times it will show the other student's assignment, when ideally I'd like to just redirect back to their home page.
Similarly, this happens with the assignment's edit page (students/:id/assignments/:id/edit), course (students/:id/courses/:id) and course's edit page (students/:id/courses/:id/edit). Sometimes I'll get an "ArgumentError in Assignments#edit" when viewing an assignment's edit page.
I believe these should be able to be remedied in my controllers, so I've included my assignments_controller and courses_controller.
Assignments_controller:
class AssignmentsController < ApplicationController
before_action :require_logged_in
before_action :set_student
def new
if #student && #student.id == current_student.id
#assignment = Assignment.new
#courses = Course.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def create
#assignment = Assignment.new(assignment_params)
#assignment.student_id = current_student.id if current_student
#courses = Course.where(student_id: current_student.id)
if #assignment.save
redirect_to student_assignments_path(#student)
else
render :new
end
end
def index
if #student && #student.id == current_student.id
#assignments = Assignment.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def show
#student = Student.find_by(id: params[:student_id])
if #student && #student.id == current_student.id
##assignment = student.assignments.find_by(id: params[:id])
#assignment = Assignment.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def edit
if #student && #student.id == current_student.id
#assignment = Assignment.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
def update
student = Student.find_by(id: params[:student_id])
#assignment = Assignment.find_by(id: params[:id])
#assignment.update(params.require(:assignment).permit(:title, :due_date))
redirect_to student_assignment_path(student, #assignment)
end
def destroy
#student = Student.find_by(id: params[:student_id])
#assignment = Assignment.find_by(id: params[:id]).destroy
redirect_to student_path(#student), notice: 'Assignment was successfully completed.'
end
private
def assignment_params
params.require(:assignment).permit(:title, :due_date, :course_id, :student_id)
end
def set_student
#student = Student.find_by(id: params[:student_id])
end
end
Courses_controller:
class CoursesController < ApplicationController
before_action :require_logged_in
before_action :set_student
def new
if #student && #student.id == current_student.id
#course = Course.new
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def create
if #student && #student.id == current_student.id
#course = Course.create(course_params)
#course.student_id = params[:student_id]
if #course.save
redirect_to student_courses_path(#student)
else
render :new
end
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def index
if #student && #student.id == current_student.id
#courses = Course.where(student_id: current_student.id)
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def show
#student = Student.find_by(id: params[:student_id])
if #student && #student.id == current_student.id
#course = #student.courses.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def edit
if #student && #student.id == current_student.id
#course = Course.find_by(id: params[:id])
else
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users courses.'
end
end
def update
student = Student.find_by(id: params[:student_id])
#course = Course.find_by(id: params[:id])
#course.update(params.require(:course).permit(:course_name))
redirect_to student_course_path(student, #course)
end
def destroy
#student = Student.find_by(id: params[:student_id])
#course = Course.find_by(id: params[:id]).destroy
redirect_to student_path(#student), notice: 'Course was successfully deleted.'
end
private
def course_params
params.require(:course).permit(:course_name)
end
def set_student
#student = Student.find_by(id: params[:student_id])
end
end
This line is the source of all the problems:
#assignment = Assignment.find_by(id: params[:id])
That's a huge mistake. I'd argue that you never, ever use the top-level model to fetch records that must be secured. The failure state of this code is a user sees everything. This is a problem that can't be fixed by patching over it with an access-control list. Those may not apply correctly every time, someone could find a loophole.
Instead you do this:
#assignment = #student.assignments.find_by(id: params[:id])
Worst-case scenario is you get a not-found error. It's impossible for someone to bypass this by hacking around with the URL. The failure state here is the record is not found.
If you want your URLs resistant to tampering you'll also want to use non-sequential identifiers. On MySQL it's often best to create a secondary column specifically for this purpose, like called param or slug or ident, whatever you prefer, and populate that with something random and harmless like:
before_validation :assign_slug
def assign_slug
self.slug ||= SecureRandom.uuid
end
Where that's indexed in your schema for quick retrieval. Where you have a student relationship:
add_index :assignments, [ :student_id, :slug ]
Postgres allows using UUID primary keys which might be verbose, but don't allow people to tinker and experiment to expose information. You really can't "guess" a randomized UUID value.
This might help you:
In your CoursesController and AssignmentsController, add a before_action in your controllers that will limit student's access.
#xxx_controller.rb
class XxxController < ApplicationController
before_action :require_logged_in
before_action :set_student
before_action :check_owner, only: [:show, :edit, :update, :destroy]
Then define the method in your ApplicationController:
#application_controller.rb
def check_owner
if #student.blank? || #student.id != current_student.id
redirect_to student_path(current_student), error: 'Sorry, you can\'t view another Users assignments.'
end
end
I guess access restriction is what you are looking for. CanCanCan will help you to set proper access to pages.
Also please replace find_by(id: params[:id]) with find(id) because it is more readable and more efficient.
So you have authentication set up allowing access only to logged in users.
Now you need an authorization process to allow access to certain actions/resources depending on the user id or role.
One popular option is Pundit and the readme page should get you started.

How does || work in?: #client = client.find(params[:client_id] || params[:id])

New to rails. Following a tutorial on polymorphic associations, I bump into this to set #client in create and destroy.
#client = Client.find(params[:client_id] || params[:id])
I'm normally only used to that you can only find #client = Client.find(params[:id])
so how does this work with there being two params? How does the || work?
FavoriteClientsController.rb:
class FavoriteClientsController < ApplicationController
def create
#client = Client.find(params[:client_id] || params[:id])
if Favorite.create(favorited: #client, user: current_user)
redirect_to #client, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #client, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#client = Client.find(params[:client_id] || params[:id])
Favorite.where(favorited_id: #client.id, user_id: current_user.id).first.destroy
redirect_to #client, notice: 'Leverandøren er nu fjernet fra favoritter'
end
end
Full code for controller, models can be seen here
Using rails 5
Expression: params[:client_id] || params[:id] is the same as:
if params[:client_id]
params[:client_id]
else
params[:id]
end
Wow thats an incredibly bad way to do it.
A very extendable and clean pattern for doing controllers for polymorphic children is to use inheritance:
class FavoritesController < ApplicationController
def create
#favorite = #parent.favorites.new(user: current_user)
if #favorite.save
redirect_to #parent, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #parent, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#favorite = #parent.favorites.find_by(user: current_user)
redirect_to #parent, notice: 'Leverandøren er nu fjernet fra favoritter'
end
private
def set_parent
parent_class.includes(:favorites).find(param_key)
end
def parent_class
# this will look up Parent if the controller is Parents::FavoritesController
self.class.name.deconstantize.singularize.constantify
end
def param_key
"#{ parent_class.naming.param_key }_id"
end
end
We then define child classes:
# app/controllers/clients/favorites_controller.rb
module Clients
class FavoritesController < ::FavoritesController; end
end
# just an example
# app/controllers/posts/favorites_controller.rb
module Posts
class FavoritesController < ::FavoritesController; end
end
You can then create the routes by using:
Rails.application.routes.draw do
# this is just a routing helper that proxies resources
def favoritable_resources(*names, **kwargs)
[*names].flatten.each do |name|
resources(name, kwargs) do
scope(module: name) do
resource :favorite, only: [:create, :destroy]
end
yield if block_given?
end
end
end
favoritable_resources :clients, :posts
end
The end result is a customizable pattern based on OOP instead of "clever" code.
The tutorial which teaches you to do
Client.find(params[:client_id] || params[:id])
is a super-duper bad tutorial :) I strongly recommend you to switch to another one.
Back to the topic: it is logical OR: if first expression is neither nil or false, return it, otherwise return second expression.
That thing is just trying to find client by client_id if there is one in the request params. If not it's trying to find client by id.
However such practic can make you much more pain than profit.

DRY voting methods

I have a prompt asking to write my up_vote and down_vote methods in only two lines using 'redirect_to' and the 'update_vote!' method as presented below. Implementing redirect_to is easy enough, but I'm not quite sure how to write my up/down_vote methods concisely using the existing 'update_vote!' method. Any help is appreciated.
class VotesController < ApplicationController
before_action :load_post_and_vote
def up_vote
if #vote
#vote.update_attribute(:value, 1)
else
#vote = current_user.votes.create(value: 1, post: #post)
end
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
def down_vote
if #vote
#vote.update_attribute(:value, -1)
else
#vote = current_user.votes.create(value: -1, post: #post)
end
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
private
def load_post_and_vote
#post = Post.find(params[:post_id])
#vote = #post.votes.where(user_id: current_user.id).first
end
def update_vote!(new_value)
if #vote
authorize #vote, :update?
#vote.update_attribute(:value, new_value)
else
#vote = current_user.votes.build(value: new_value, post: #post)
authorize #vote, :create
#vote.save
end
end
end
You should invoke the update_vote! method. How about:
def up_vote
update_vote!(1)
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
def down_vote
update_vote!(-1)
# http://apidoc.com/rails/ActionController/Base/redirect_to
redirect_to :back
end
Also your method can be re-written as :
def update_vote!(new_value)
Vote.find_or_create_by post: #post do |v|
authorize v, :update?
v.user_id = current_user.id
v.value = new_value
v.save!
end
end
Read find_or_create_by.
seems that answer was easy enough =]
def up_vote
update_vote(1)
redirect_to :back
end
def down_vote
update_vote(-1)
redirect_to :back
end

Trouble Creating and showing Users in Rails blog

I keep getting a variety of error while trying to create and show errors in a simple Rails blog I'm trying to create.Let me know if you see anything obvious or if you need me to post more code as I've tried a number of things but to no avail. Thanks
The browser is giving me this error
Couldn't find User without an ID
in my "logged_in?" method which shows
def logged_in?
#current_user ||= User.find(session[:user_id])
end
Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
flash[:success] = "You are logged in"
redirect_to root_path
else
render action: 'new'
flash[:error] = "There was a problem logging in. Please check your email and password"
end
end
end
def index
#users = User.all
end
def show
end
def new
#user = User.new
end
def edit
end
def create
#user = User.new(user_params)
if #user.save
session[:user_id] = #user.id
flash[:notice] = "You have registered, please login"
redirect_to login_path
else
render :new
end
end
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to #user, notice: 'User was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
def destroy
#user.destroy
respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
end
Articles Controller
class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def new
#article = Article.new
end
def index
#article = Article.all
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
def edit
#article = Article.find(params[:id])
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render 'edit'
end
end
def show
#article = Article.find(params[:id])
end
def destroy
#article = Article.find(params[:id])
#article.destroy
redirect_to articles_path
end
private
def article_params
params.require(:article).permit(:title, :text, :image)
end
end
Application Helper
module ApplicationHelper
def logged_in?
#current_user ||= User.find(session[:user_id])
end
end
The problem you're facing is that session[:user_id] is nil. Usually a method which sets current user is called current_user. The logged_in? is not a good name for a method setting an user instance, because one would expect that a method ending with a question mark would return a true or false. And not an user instance.
Also, setting the current user is usually done with a before_filter. Additionally, you want to skip such before filter for action where you're setting the current user (i.e the current_user doesn't exist yet)
Finally, I would rather fail gracefully, if user is not found. You can achieve this by changing your code to User.find_by_id(session[:user_id])
While the user is not loggued, session[:user_id] is nil, and so User.find(session[:user_id]) generates the error. The method should be like this:
def logged_in?
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
Why would the logged_in? helper method try to assign a value to #current_user? I think that is a bad logic, it should just return a boolean result without modifying such a central instance. This is a proper way to do that:
def logged_in?
#current_user.nil? ? false : true
end
The responsibility of setting the #current_user falls to a method that you should place in application_controller.rb and make it a before_action so that it's executed before any controller action is triggered, that is:
# app/controllers/applicaton_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_action :authenticate_user
# Your actions here
..
..
#
private
def authenticate_user
#current_user ||= User.find(session[:user_id]) if session[:user_id].present?
end
end

ArgumentError in UsersController#update

I have not changed my users_controller file, but now I am receiving this error after updating user profile "wrong number of arguments (2 for 1)". It points to "app/controllers/users_controller.rb:41:in `update'".
My code looks to be right but I have to be missing something. Any ideas?
users_controller:
class UsersController < ApplicationController
def new
#user = User.new
end
def profile
#profile = User.profile
end
def create
#user = User.new(params[:user])
if #user.save
UserMailer.registration_confirmation(#user).deliver
session[:user_id] = #user.id
redirect_to root_url, notice: "Thank you for signing up!"
else
render "new"
end
end
def show
#user = User.find(params[:id])
end
def edit
#user = User.find(params[:id])
end
def index
#users = User.all
end
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted."
redirect_to users_url
end
def update
#user = if current_user.role?(:admin)
User.find(params[:id])
else
current_user
end
if #user.update_attributes(params[:user])
flash[:success] = "Account updated"
redirect_to #user
else
render 'edit'
end
end
end
I found the fix for what I was having troubles with.
In user.rb I had to add:
def has_role?(role_name)
role.present? && role.to_sym == role_name.to_sym
end
and then in the controller I just added:
#user = if current_user.has_role?(:admin)
Before it was missing the ".has" part to it.

Resources