ruby-on-railsmodel-view-controllerparametersactioncable

Rails remote form parameter not being passed to controller


I have a form in my rails app where a user just enters a number and the form is then submitted with ajax. A new game is created in my gamescontroller if all the parameters are present. I have two hidden_field_tags whose parameters pass to the controller very nicely but the most important parameter, the one gotten from user input, doesn't seem to pass to the controller.

My form:

<%= form_for @game, :url => {:controller => "games", :action => "create" }, :html => { :role => 'form' }, remote: true, method: :post do |f| %>
  <div class="row">
    <div class="col-md-4 col-md-offset-4">
      <div class="input-group">
        <%= f.text_field :user_stake, class: 'form-control' %>
        <span class="input-group-btn">
          <%= f.submit 'Go', :html => { :type => "button" }, class: "btn btn-default" %>
        </span>
      </div>
    </div>
  </div>

    <%= hidden_field_tag 'user_id', current_user.id  %>
    <%= hidden_field_tag 'jackpot_id', @jackpot.id  %>

<% end %>

Controller:

 before_action :game_params, only: [:create]

  def create
    @game = Game.new(game_params)
    if @game.save
      @jackpot = Jackpot.find(params[:jackpot_id])
      ActionCable.server.broadcast 'jackpot_channel',
                                        users: Jackpot.where(id: @jackpot.id),
                                        pot: @jackpot,
                                        user: User.find(@game.user_id),
                                        game: @game,
                                        stake: @game.user_stake.to_s
    else
      ActionCable.server.broadcast 'jackpot_channel',
                                        error: @game.errors.full_messages
    end
  end

  def new
    @game = Game.new
  end

  private
    def game_params
      params.permit(:user_stake, :user_id, :jackpot_id)
    end

No matter what is typed in the @game gets saved with a user_stake of 0.0, which I have set as the default in my migrations. I have no idea what I'm doing wrong here. Any ideas? Thanks!


Solution

  • You're not nesting the inputs properly:

    <%= form_for @game, html: { role: 'form' }, remote: true do |f| %>  
      <%= f.text_field :user_stake, class: 'form-control' %>
      <%= hidden_field_tag 'user_id', current_user.id  %>
      <%= hidden_field_tag 'jackpot_id', @jackpot.id  %> 
      # ..
    <% end %>
    

    This would give the following params hash:

    {
       game: {
         user_stake: 1.2
       },
       user_id: 3,
       jackpot_id: 4
    }
    

    If you send that through the whitelist you get:

    {
       user_id: 3,
       jackpot_id: 4
    }
    

    One solution is to simply nest the inputs:

    <%= form_for @game, html: { role: 'form' }, remote: true do |f| %>  
      <%= f.text_field :user_stake, class: 'form-control' %>
      <%= f.hidden_field_tag 'user_id', current_user.id  %>
      <%= f.hidden_field_tag 'jackpot_id', @jackpot.id  %> 
      # ..
    <% end %>
    

    And whitelist them properly:

    private
      def game_params
        params.require(:game)
              .permit(:user_stake, :user_id, :jackpot_id)
      end
    

    But there is a huge warning flag here - NEVER pass the current user id through the params as it makes it really easy for a malicous user to hack with nothing but the web inspector. Instead use the value directly from the session.

    You can't fake that except with the users password or by cracking the session encryption.

    Also if a game belongs to a jackpot I would set it up as a nested resource and put the id in the path as this creates a RESTful structure that clearly shows that you are adding children to a parent resource - instead of hiding that vital information in the request body.

    # routes.rb
    resources :jackpots do
      resources :games, shallow: true
    end
    

    class GamesController
      before_action :set_jackpot, only: [:new, :create, :index]
    
      # GET /jackpots/:jackpot_id/games/new
      def new
        @game = @jackpot.games.new
      end
    
      # POST /jackpots/:jackpot_id/games
      def create
        @game = @jackpot.games.new(game_params) do |g|
          g.user = current_user
        end
    
        if @game.save
          # ...
        else
          # ...
        end
      end
    
      # GET /jackpots/:jackpot_id/games
      def index
        @games = @jackpot.games
      end
    
      private 
    
        def set_jackpot
          @jackpot = Jackpot.includes(:games)
                            .find(params[:jackpot_id])
        end
    
        def game_params
          params.require(:game).permit(:user_stake)
        end
    end
    

    <%= form_for [@jackpot, @game], remote: true, html: { role: 'Form' } do |f| %>
      <div class="row">
        <div class="col-md-4 col-md-offset-4">
          <div class="input-group">
            <%= f.number_field :user_stake, class: 'form-control' %>
            <span class="input-group-btn">
              <%= f.submit 'Go', :html => { :type => "button" }, class: "btn btn-default" %>
            </span>
          </div>
        </div>
      </div>
    <% end %>
    

    Note how there are no hidden inputs required.