ruby-on-railsrubyformsactioncontroller

How to use a rails form partial in the context of another controller without passing the ID as a hidden field


I'm trying to embed a Comment form to a rails Post show view and the only way I can get it to work is by passing this hidden field in the comment form:

<%= form.hidden_field :post_id, value: "#{params[:id]}" %>

Here is my Post show action:

def show
  @comment = Comment.new
end

Here is the Comment create action:

def create
  @user = current_user
  @comment = @user.comments.build(comment_params)
end

I tried adding this to the Comment create action, but it still said the Post ID was missing:

def create
  @user = current_user
  @post = Post.find(params[:id])
  @comment = @user.comments.build(comment_params).merge(post_id: @post.id)
end

I also tried adding the @post = Post.find(params[:id]) to the Post show action, thinking that if rails had that variable then the Comment create action would have access to @post.id).

The only thing that works is adding the post_id as a hidden field in the Comment form, but this seems dangerous because a malicious user could edit the html in the browser. I don't know why they would want to do this just to change the Post that the comment gets applied to, but it still seems not the right way to do this.

I don't want a "nested form" in the sense that the comment is something that is created via the post form.

It's really just a separate Comment form on the Post show page. I'm assuming this is a common thing in Rails, but having trouble figuring out the "correct" way to do this.


Solution

  • The Rails way to do this is is by declaring a nested resource:

    # config/routes.rb
    resources :posts do
      resources :comments, only: [:create]
    end
    

    This creates the route POST /posts/:post_id/comments which connects the two resources in a RESTful way and makes it very transparent what is going on compared to placing the post id in the request body.

    # app/views/comments/_form.html.erb
    <%= form_with(model: [@post, @comment], local: true) do %>
      # don't create a hidden input since the 
      # post id is passed through the URL
    <% end %>
    
    # app/views/posts/show.html.erb
    <%= render partial: 'comments/form' %>
    
    # app/views/comments/new.html.erb
    <%= render partial: 'comments/form' %>
    
    class CommentsController < ApplicationController
      before_action :set_post, only: [:create]
    
      # POST /posts/1/comments
      def create
        @comment = @post.comments.new(comment_params) do |c|
          c.user = current_user
        end
        if @comment.save
          redirect_to @post, success: 'Comment Created'
        else
          render :new
        end
      end
    
      private
      def set_post
        @post = Post.find(params[:post_id])
      end
    end
    

    The only thing that works is adding the post_id as a hidden field in the Comment form, but this seems dangerous because a malicious user could edit the html in the browser. I don't know why they would want to do this just to change the Post that the comment gets applied to, but it still seems not the right way to do this.

    You're thinking about the problem wrong. If users should only be able to comment on certain posts you need to enforce authorization on your server (such as by using Pundit or CanCanCan).

    What is really bad is passing the current user id in hidden inputs as it makes it very easy for malicous users to create resources as another user.

    # This is how you get pwned
    <%= form.hidden_field :user_id, value: current_user.id %>
    

    You want to rely on the session storage instead as its encrypted and harder to tamper with.