I have an action in a controller that looks like this:
def show
#project = current_customer.projects.where(id: params[:project_id]).first
if #project
#feature = #project.features.where(id: params[:feature_id]).first
if #feature
#conversation = #feature.conversations.where(id: params[:id]).first
unless #conversation
head 401
end
else
head 401
end
else
head 401
end
end
The problem is the repetition of head 401. Is there a better way to write this action ?
I would write it like this
def show
#project = current_customer.projects.where(id: params[:project_id]).first
#feature = #project.features.where(id: params[:feature_id]).first if #project
#conversation = #feature.conversations.where(id: params[:id]).first if #feature
# error managment
head 401 unless #conversation
end
Maybe you can refactor in your project model with something like this
Model Project
...
def get_conversation
feature = features.where(id: params[:feature_id]).first
conversation = feature.conversations.where(id: params[:id]).first if feature
end
And in your Controller
Controller ProjectController
def show
#project = current_customer.projects.where(id: params[:project_id]).first
#conversation = #project.get_conversation
head 401 unless #conversation
end
Related
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.
So it's been quite a bit of time since I played with relationships and I want to make sure I've done it right.
In my model for Client I have:
class Client < ApplicationRecord
has_many :projects, dependent: :destroy
end
In my model for Projects I have:
class Project < ApplicationRecord
belongs_to :client
end
So I know that's set. Then to grab projects I put in my projects controller:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would I need to put something in my show that does the same?
Anything else I'm missing for relationships?
I would think this:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would look more like:
def create
#client = Client.find(params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
end
Note that
#project = #client.project.new(project_params)
should be:
#project = #client.projects.new(project_params)
As Yechiel K says, no need to do:
#client.project << #project
Since:
#project = #client.projects.new(project_params)
will automatically set client_id on the new #project. BTW, if you want to add a project to the client manually, then it's:
#client.projects << #project
(Note projects vs. project.)
In the off chance that there is not a client with params[:client_id], then #client = Client.find(params[:client_id]) will throw an error. You should probably include a rescue block. Alternatively, I prefer:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
else
# do something when client not found
end
end
Also, respond with isn't a thing. respond_with is a thing. (I believe it's been moved to a separate gem, responders.) It's unclear from your code if you're needing different responses, say, for html and js. If not, then I think it would be more like:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [#client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
This assumes that your routes look something like:
Rails.application.routes.draw do
resources :clients do
resources :projects
end
end
In which case rails will resolve [#client, #project] to the correct route/path.
As DaveMongoose mentions, you could move #client = Client.find_by(id: params[:client_id]) into a before_action. This is quite common. Here's one discussion of why not to do that. Personally, I used to use before_action like this, but don't any more. As an alternative, you could do:
class ProjectsController < ApplicationController
...
def create
if client
#project = client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
end
Taking this a bit further, you could do:
class ProjectsController < ApplicationController
...
def create
if client
if new_project.save
flash[:notice] = "Project created successfully"
redirect_to [client, new_project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
def new_project
#new_project ||= client.projects.new(project_params)
end
end
I would replace this line:
flash[:notice] = "Project created successfully" if #client.project << #project
with:
flash[:notice] = "Project created successfully" if #project.save
No need to manually add #project to #client.projects, it gets added automatically when you create it using #client.projects.new, the only thing you missed was that creating something using .new doesn't persist it in the DB, that gets accomplished by calling #project.save.
For your show action, I'm not sure if you mean the client's show page or the project's, but in either case, you would retrieve it using params[:id] (unless you were using some nested routing).
Can someone tell me why id and category_id keep getting switched in Rails?? im losing my mind! the console says its correctly but when i get an error it shows that params switches it around. This my controller below:
class ArticlesController < ApplicationController
def new
#article = Article.new
#category = Category.find(params[:category_id])
end
def create
#article = Article.new(articles_params)
#category = Category.find(params[:category_id])
# binding.pry
#article.category_id = #category.id
binding.pry
if #article.save
redirect_to category_path(#category)
else
render :new
end
end
def edit
#category = Category.find(params[:category_id])
#article = Article.find(params[:id])
end
def update
#article = Article.find(params[:id])
#category = Category.find(params[:category_id])
if #article.update(articles_params)
redirect_to category_path(#category)
else
render :edit
end
end
def destroy
# binding.pry
#article = Article.find(params[:id])
#article.destroy
redirect_to category_path(#category)
end
private
def articles_params
params.require(:article).permit(:title, :body)
end
end
Everything looks fine.
Look below on the request parameters from your screenshot:
{
"id" : 1,
"category_id" : 2
}
You are looking for id 1 in the context of your Article controller.
1 refers to the article with id 1.
Meanwhile you are looking for a Category with id 2 (category_id 2).
Category.find(params[:category_id])
The error is saying that it cannot find a Category object with id 2.
The 2 comes from your request. If something is wrong, it's in your request creation.
Im using a gem called MetaInspector to scrape data from different websites. Im building a site where i can collect data from different sites but am having trouble setting up. I have a model called site with a title and a url both strings. When i create a new "site" the name will come out as example.com/"sitename" and in there i would like to have the data just from that site. I kinda have an idea to this by adding page = MetaInspector.new to the new method but cant see how i can set a url in there.
I can show my controller and other info if needed.
Controller
class Admin::SitesController < Admin::ApplicationController
def index
#sites = Site.all
end
def show
#site = Site.friendly.find(params[:id])
end
def edit
#site = Site.friendly.find(params[:id])
end
def update
#site = Site.friendly.find(params[:id])
if #site.update(site_params)
redirect_to admin_path
else
render :edit
end
end
def destroy
#site = Site.friendly.find(params[:id])
#site.destroy
if #site.destroy
redirect_to admin_path
end
end
def new
#site = Site.new
end
def create
#site = Site.new(site_params)
if #site.save
redirect_to admin_path
else
render :new
end
end
private
def site_params
params.require(:site).permit(:title, :url)
end
end
If I understand correct you want to show the metainfo for a Site you have added. You could put that code in the show action of the controller:
def show
#site = Site.friendly.find(params[:id])
#page = MetaInspector.new(#site.url)
end
And update the show.html.erb template to display info about #page, ie:
<%= #page.title %>
I'm re-doing an assignment that is a clone of Imgur.
I tried adding a picture to the database with an artist, title and URL: but now I have an error stating my param are missing or the value is empty. I've searched and searched and I'm not getting any closer.
Here is my pictures_controller.rb:
class PicturesController < ApplicationController
def index
#picture = Picture.all
end
def show
#picture = Picture.find(params[:id])
end
def new
#picture = Picture.new
end
def create
#picture = Picture.new(picture_params)
if #picture.save
redirect_to pictures_url
else
render :new
end
end
def edit
#picture = Picture.find(params[:id])
end
def update
#picture = Picture.find(params[:id])
if #picture.update_attributes(picture_params)
redirect_to "/pictures/#{#picture.id}"
else
render :edit
end
end
def destroy
#picture = Picture.find(params[:id])
#picture.destroy
redirect_to pictures_url
end
private
def picture_params
params.require(:picture).permit(:artist, :title, :url)
end
end
I really appreciate your help, all. I'm completely stumped! And it's probably something really small, as usual :-\ Thank you.