loading...
Cover image for StimulusReflex: Working with user roles in rails views

StimulusReflex: Working with user roles in rails views

shivashankarror profile image Shivashankar ・4 min read

Introduction

In almost every rails application that uses authentication, we have a set of user roles, at least 'admin' and 'user' roles.

In Rails views, we show/hide certain critical links or sections based on user roles. This post is about how to implement the same with StimulusReflex.


Assume we have tasks model with fields title and status.
a) Admin view tasks as follows

Title Status
Fix homepage css issue Mark as resolved Remove
Add new banner Mark as resolved Remove
Iphone support Resolved Remove

b) Non Admin user view tasks as follows

Title Status
Fix homepage css issue Pending
Add new banner Pending
Iphone support Resolved

Here 'Admin' only can mark a task as 'Resolved' or can remove a task.

So in rails view we need to find the current user role. If current user is admin we shall show the Mark as resolved link and Remove link else we do not.

It is very simple when we work in normal rails application. we can directly use current_user method and get user role.
But when we work with Stimulus Reflex it is not enough.
We need some tweaks in that.

This post covers how to add , edit and remove record based on user role.

Step 1: Add stimulus_reflex and cable_ready gems in Gemfile

gem 'cable_ready'
gem "stimulus_reflex", "~> 3.2"
$ bundle install

Step 2: create scaffold and migrate

$ rails g scaffold User name email role
$ rails g scaffold Task title description resolved:boolean
$ rake db:migrate

Step 3: update app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        if verified_user = User.find_by(id: cookies.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

Step 4: create tasks channel

rails g channel Tasks 

Step 5: update tasks channel app/channels/tasks_channel.rb

class TasksChannel < ApplicationCable::Channel
  delegate :current_user, to: :connection

  # Instead of single channel 'tasks' now we need separate 
  # channels based on user roles.  
  def subscribed
    stream_from channel_name
  end

  def channel_name
   # assuming role does not has any whitespace
   "tasks-#{current_user.role}"
  end
end

Step 6: Update app/javascript/channels/tasks_channel.js

import cableReady from 'cable_ready'
import consumer from "./consumer"

consumer.subscriptions.create("TasksChannel", {
  received(data) {
    if (data.cableReady) cableReady.perform(data.operations)
  }
});

Step 7: Generate tasks reflex

$ rails g stimulus_reflex Tasks

Step 8: Add available user roles in User model

class User < ApplicationRecord
  def self.roles
    ['admin', 'user']
  end

  def admin?
    role == 'admin'
  end 
end

Step 9: Update app/reflexes/tasks_reflex.rb

class TasksReflex < ApplicationReflex
  include CableReady::Broadcaster

  def update
    # verify current_user is admin? 
    # raise exception or appropriate error message here.
    return unless current_user.admin?

    id = element.dataset[:id]
    task = Task.find(id)
    task.update(resolved: true)

    User.roles.each do |role|
      channel_name = "tasks-#{role}"
      cable_ready["tasks"].text_content(
        selector: "status-col-#{id}",
        text: 'Resolved'
       )
      cable_ready.broadcast
    end 
  end 

  def remove(id)
    # verify current_user is admin? 
    # raise exception or appropriate error message here.
    return unless current_user.admin?

    task = Task.find(id)
    task.destroy 

    # in views we have set id for each row
    # When admin remove a row both admin and non-admin users 
    # channel receives notification

    User.roles.each do |role|
      channel_name = "tasks-#{role}"
      cable_ready["tasks"].remove(
        selector: "row-#{id}"
      )
      cable_ready.broadcast
    end
  end
end

Step 10: Update app/javascript/controllers/tasks_controller.js

import ApplicationController from './application_controller'
export default class extends ApplicationController {
  remove(event) {
    event.preventDefault();
    // This is where we are prompting the user for confirmation
    const ok = confirm("Are you sure to mark the task as 'resolved'?")
    if(!ok){
      return false;
    }else{
      const el = event.currentTarget
      const id = el.getAttribute("data-id");
      this.stimulate("TasksReflex#delete", id)
    }
  }
}

Step 11: Update app/controllers/tasks_controller.rb

class TasksController < ApplicationController
  include CableReady::Broadcaster

  def index
    @tasks = Task.all
  end  

  # Handling new task creation
  def create
    @task = Task.new(task_params)
    if @task.save
      # broadcast changes to all users
      User.roles.each do |role|
        cable_ready["tasks-#{role}"].insert_adjacent_html(
          selector: "#tasks",
          position: "beforeend",
          html: render_to_string(partial: "task", locals: {admin: (role == 'admin'), task: @task})
        )
        cable_ready.broadcast
      end 
    end  
  end  

  def task_params
    params.require(:task).permit(:title, :description)
  end
end

Step 12: Update app/views/tasks/index.html.erb

    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Title</th>
          <th>Status</th>
          <th></th>
        </tr>
      </thead>
      <tbody id="tasks" data-reflex-root="#tasks">
        <!-- pass `admin` in locals which can be used while broadcasting with reflex --> 
        <% @tasks.each do |task| %>
          <%= render 'task', collection: @tasks, locals: {admin: current_user.admin?} %>
        <% end %>
      </tbody> 
     </table>

Step 13: Update app/views/tasks/_task.html.erb

    <!-- unique row id  -->
      <tr id="row-<%= task.id %>">
        <td><%= task.title %></td>
        <td id="status-col-<%= task.id %>">
          <!-- TODO: Move this to helper or decorator -->
          <% if admin && !task.resolved %>
             <%= link_to 'Mark as resolved', '#',  data:{reflex: 'click->TasksReflex#update', id: task.id} %>
          <% else %>
            <%= task.resolved? ? 'Resolved' : 'Pending'
          <% end %>

        </td>
        <td data-controller="tasks">
          <!-- Show the 'Remove' option only for admin -->
          <% if admin %>
             <!-- Call controller instead of Reflex to show confirmation --> 
             <%= link_to 'Remove', '#', data:{action: "click->tasks#remove", id: task.id} %> 
          <% end %>
        </td>
      </tr>

By having different channels based on roles now we can easily apply role based restriction to any section in rails views

Posted on Jun 29 by:

shivashankarror profile

Shivashankar

@shivashankarror

An enthusiastic Ruby on Rails developer

Discussion

markdown guide