Rails 5, action cable and current_user
Asked Answered
M

2

6

So I have this use case where I render a message in a controller (with ApplicationController.renderer) that is then broadcasted to a couple of users. The broadcast also is performed in inside the same controller. Both these actions are triggered when an update to certain object is performed.

The problem is, I need to access the current_user object inside that rendered view, and of course, I can not render it with the current user as a local variable because then the message will be sent with the user that broadcasted the message and not the end user that will see that view.

So, after reading a couple of blog posts and the Rails docs I set the authentication with cookies to be supported by action cable.

My question is: how can I access, inside the rendered view, the object (current_user) of the end user?

Currently, my connection class looks like this. However, how can I render that view with this variable (logged_user)?

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

   def connect
     self.logged_user = User.find_by(id: cookies.signed[:user_id])
   end
 end
end

My controller looks like this:

(...)

 def update 
   if @poll.update(poll_params)
     broadcast_message(render_message(@poll), @poll.id, @poll.room.id)
     (...)
   end
 end

 def broadcast_message(poll = {}, poll_id, room_id)
   ActionCable.server.broadcast 'room_channel', body: poll, id: poll_id, room_id: room_id
 end

 def render_message(poll).
   if poll.show_at.to_time <= Time.now
     ApplicationController.renderer.render(
       partial: 'rooms/individual_student_view_poll',
       locals: {
         poll: poll,
         room: @room
       })
   end
 end

(....)

So basically, my ultimate goal is to access the logged_user object after the message is broadcasted to it.

Thanks

Martinelli answered 12/1, 2017 at 18:35 Comment(2)
I have the exact same problem, did you figure it out in the meantime?Pyrrolidine
you could render a partial that return some javascript that will fire a new request from the client side to fetch whatever need to render, this way this request will be set with the proper context. But if you have let's say 100 users subscribed to that channel, each one of them will fire a single request to fetch that new content.Leptosome
B
0

Something like this:

ApplicationController.renderer.render(
  partial: 'rooms/individual_student_view_poll',
  locals: {
    poll: poll,
    room: @room
  },
  assigns: {
    logged_user: logged_user
  }
)

Will be available in your template like this:

<% if defined? @logged_user %>
...
<% end %>
Burmaburman answered 12/1, 2017 at 18:59 Comment(6)
Ok but how can I access the var logged_user from the controller?Martinelli
By using assigns, the { logged_user: ... } become @logged_user. Your channel inherits ApplicationCable::Channel so you can access self.logged_user from your channel and transmit to your view via "assigns"Burmaburman
Yes but as I said previously I am rendering from the controller not from the channel. should I do it on the channel? If yes, and assuming that the render_message method resides in the channel, how can I access to it within the controller?Martinelli
If you want to broadcast from controller to a specific user, you can simply use a channel name integrating its username for example. Or you can integrate this code in user model. Its depends of your project structure.Burmaburman
I want to broadcast to all users.Martinelli
If a user post a message from ActionCable client, your action will be called from the specified channel. Then you can broadcast your specific message from this action call to any channel with the data you want.Burmaburman
R
0

I've had to solve the same problem. What I wound up doing was creating a template renderer that inherits from ApplicationController that I could configure just for rendering templates with a current_user assigned before the render. Here is what that looks like:

class TemplateRenderer < ApplicationController
  def self.with_current_user(user)
    @current_user = user
    self
  end

  def self.current_user
    @current_user
  end

  def current_user
    self.class.current_user
  end
  helper_method :current_user
end

With this in place, you can simply call:

TemplateRenderer.with_current_user(logged_user).render(...)

Now calling current_user in your partial will access the helper method defined on TemplateRenderer, which in turn will refer to the class instance variable set by with_current_user.

As you may have noticed, if you use the assigns option in the render call instead, you could access that variable by referring directly to the assigned instance var (@current_user), but not by calling the current_user helper, even if that helper was set to return the @current_user instance variable. Having a special controller/renderer to override the current_user at the class level before calling render gets around this issue.

To make things easier, I had with_current_user return the class instance itself so that it could be easily chained with render.

I did wrestle with the naming and location of this class. TemplateRenderer describes its intended purpose, but it inherits from ApplicationController, which would suggest a name with a Controller suffix. I chose to locate this in my channels directory for now and leave off the suffix, since its only purpose (in my case) is to render templates for channels. But I can easily see arguments for putting it in the controllers directory with a Controller suffix.

EDIT: While the above answer will work, there is a subtle danger with it. If you call were to call TemplateRenderer.render again after the first call, the current user would still be set, which could result in users seeing private content for another user. To solve this, you can do the following:

class TemplateRenderer < ApplicationController
  def self.render_for_user(user, *args)
    @current_user = user
    res = render(*args)
    @current_user = nil
    res
  end

  def self.current_user
    @current_user
  end

  def current_user
    self.class.current_user
  end
  helper_method :current_user
end

Now, calling:

TemplateRenderer.render_for_user(logged_user, partial: "my_partial", **other_opts)

...will render with the specified user as current_user, but then set current_user to nil after the render.

Ringler answered 15/10, 2021 at 13:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.