StimulusReflex: Working with user roles in rails views 
Tags: rails, stimulusreflex
Published: 2020-06-18 09:54:16

###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

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

Step 2: create scaffold and migrate
```shell
$ 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`
```ruby
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
```shell
rails g channel Tasks 
```

Step 5: update tasks channel `app/channels/tasks_channel.rb`
```ruby
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
```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
```shell
$ rails g stimulus_reflex Tasks
```

Step 8: Add available user roles in User model
```ruby
class User < ApplicationRecord
  def self.roles
    ['admin', 'user']
  end
  
  def admin?
    role == 'admin'
  end 
end
```

Step 9: Update `app/reflexes/tasks_reflex.rb`
```ruby
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`

```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`
```ruby
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`

```html
    <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`
```html
    <!-- 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