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
Top comments (0)