I'm using Activeadmin to make a dashboard page and Devise for User authorize. I have 2 model are Member (regular user) and User(admin user). When I use the Action cable feature to show the user appearance or not then just only Member user received these broadcast. The problem is how the User model can receive these broadcast too ?
Thank you for your time and sorry for my clumsy English
My connection
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_member
def connect
self.current_member = find_verified_member
# self.current_user = find_verified_member
logger.add_tags 'AcctionCable', current_member.email
end
protected
def find_verified_member
# if current_member = Member.find_by(id: cookies.signed[:member_id])
if verified_member = env['warden'].user
verified_member
else
reject_unauthorized_connection
end
end
end
end
If you want to make a chat with actioncable and activeadmin here is an example of a gem that does that. active_admin_chat
Or maybe you can try the piece of code below.
def connect
self.current_member = find_verified_member
end
protected
def find_verified_member
cookies_signed = cookies.signed
admin_user_id = cookies_signed['admin_user.id']
if admin_user_id
AdminUser.find(admin_user_id)
else
User.find(cookies_signed['user.id'])
end
end
Looks like you want to broadcast to a channel for users whenever a member establishes connection with ActionCable. If so, in the def subscribed method for whatever channel member is subscribing to that you wish for the admin users to be notified of, run a broadcast job to a channel accessible to all admin users. Admin members can establish a client to subscribe to this channel in order to receive these broadcasts.
Related
I have 2 user types in my application (worker and company). Both user types are created with Devise. I'm currently trying to send a notification to a specific company with ActionCable.
My main problem is that when I send a notification every single company that's signed in receives it. I get that I'm supposed to include the company id in the stream name in some way, but I haven't had any luck so far.
I've included the working code that sends notifications to all companies below:
notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end
Call to the broadcast
ActionCable.server.broadcast 'notifications_channel', { 'My data' }
EDIT
I log the status of the notification with javascript:
notifications.js
App.notifications = App.cable.subscriptions.create("NotificationsChannel", {
connected: function() {
console.log("connected");
};
disconnected: function() {
console.log("disconnected");
};
received: function(data) {
console.log("recieved");
};
});
Broadcast the message from your controller like this:
# Broadcast your message
ActionCable.server.broadcast "notifications_channel:#{target_user.id}
Now Update app/channels/application_cable/connection.rb with the below code
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.name
end
protected
def find_verified_user
verified_user = User.find_by(id: cookies.signed['user.id'])
if verified_user && cookies.signed['user.expires_at'] > Time.now
verified_user
else
reject_unauthorized_connection
end
end
end
end
And subscribe to the stream like this:
def subscribed
stream_from "notifications_channel:#{current_user.id}"
end
Note: This is just an example to show how to target a specific user in Actioncable. You may have to modify the code based on
your requirement.
I also recommend watching this video by GoRails.
I managed to find the solution. Following Abhilashs answer took me most of the way, but I still had trouble authenticating the company. It seems like Warden was not fully configured, so this post made it work: env['warden'] not working with Rails 5
I develop a Ruby on Rails 5.1 application using ActionCable. User authentification via Devise works fine for several channels. Now, I want to add a second type of channels which does not require any user authentification. More precisely, I would like to enable anonymous website visitors to chat with support staff.
My current implementation of ApplicationCable::Connection for authenticated users looks like this:
# 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
protected
def find_verified_user
user = User.find_by(id: cookies.signed['user.id'])
return user if user
fail 'User needs to be authenticated.'
end
end
end
Anonymous users will be identified by some random UUID (SecureRandom.urlsafe_base64).
Question:
How do I best add this new type of channels? Could I add a boolean flag require_authentification somewhere, override it in my inherited channel class for anonymous communication, and switch the identification method in Connection depending on this attribute? Or would I rather have to implement a completely new module, say AnonymousApplicationCable?
Hi I came into the same problem, after looking at your solution in rails github comment, I assume it is better to create the token and keep the logic in the connect method.
So what I do was just utillize the the warden checking and if it is nil just create the anonymous token and otherwise. For this to work, I need to declare 2 identifier :uuid and :current_user
class Connection < ActionCable::Connection::Base
identified_by :current_user, :uuid
def connect
if !env['warden'].user
self.uuid = SecureRandom.urlsafe_base64
else
self.current_user = find_verified_user
end
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
I have the following code which sends an ActionCable broadcast in my Rails application:
ActionCable.server.broadcast 'notification_channel', notification: 'Test message'
The connection looks as follows:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
def session
cookies.encrypted[Rails.application.config.session_options[:key]]
end
protected
def find_verified_user
User.find_by(id: session['user_id'])
end
end
end
However all users logged into the app will receive it. The identified_by only makes sure that logged in users can connect to the channel but it doesn't restrict which users get the broadcast.
Is there a way to only sending a broadcast to a certain user?
The only way I could think of doing it was:
ActionCable.server.broadcast 'notification_channel', notification: 'Test message' if current_user = User.find_by(id: 1)
Where the 1 is the ID of the user I want to target.
For user-specific notifications I find it useful to have a UserChannel where the subscription is based on the current user:
class UserChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
This way ActionCable creates a separate channel for each user, and you can use commands like this based on the user object:
user = User.find(params[:id])
UserChannel.broadcast_to(user, { notification: 'Test message' })
That way this channel can handle all user-specific broadcasts.
You should use a channel that's specific to that user. For example:
"notifications_channel_#{current_user.id}"
This is also documented in an example from the actioncable repo here: https://github.com/rails/rails/tree/master/actioncable#channel-example-2-receiving-new-web-notifications
I'm struggling with action cable. In my case I have couple of users (via Devise) who can share tasks with each other.
Now when user#1 share task (via Form) with user#2 all authenticated users receive notifications.
How and where should I identify my user#2 to broadcast only to him?
Here is my code so far:
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.id
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
cable.js
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify
ActionCable.server.broadcast "todo_channel_#{current_user.id}", message: 'some message'(not implemented yet)
end
end
todo.coffee
App.todo = App.cable.subscriptions.create "TodoChannel",
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: ->
#perform 'notify'
i've faced something similar before until i realized that you can actually call stream_from multiple times in the channel and that user will be subscribed to multiple different "rooms" within the same channel connection. Which means you can basically do this
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_all"
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(data)
# depending on data given from the user, send it to only one specific user or everyone listening to the "todo_channel_all"
if data['message']['other_user_id']
ActionCable.server.broadcast "todo_channel_#{data['message']['other_user_id']}", message: 'some message'
else
ActionCable.server.broadcast "todo_channel_all", message: 'some message'
end
end
end
that code assuming that the user already knows the other user's id and sent it to the channel, you would probably have to wrap that with some security or something, i admit i'm not very well experienced with rails as i'm still learning.
Something else that might be beneficial to you in the future is the fact that you can also broadcast several messages/times in the same channel function. That means you can potentially support sending out your tasks to a single specific user, a list of specific users or everyone. Just iterate on the list/array/whatever of users and broadcast the task/message/notification/whatever to them each on their personal "todo_channel_#{user.id}"
I ended up with a different approach. I'll write it here in case someone will find it helpful.
Notification has an id of a user that has to be notified. So in model I have:
after_commit :broadcast_notification, on: :create
def broadcast_notification
ActionCable.server.broadcast "todo_channel_#{self.user_id}", message: 'some message'
end
As I placed broadcasting into the model my todo_channel.rb looks like this:
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Step#1: Let each user have unique session token. While subscription, each user will send session token in headers and header is accessible in connection class. Find user by using session token.
Step#2: Stream user on user id.
Step#3: While sharing task, take user id too in the request and broadcast on given user id.
This is called "private chat". If you really want to get current_user.id you could do it in 3 ways:
Some AJAX onload call from todo.coffee to server.
Render current_user.id as an attribute in Rails HTML view and then get it via jQuery inside todo.coffee (as in https://www.sitepoint.com/create-a-chat-app-with-rails-5-actioncable-and-devise/ )
Create a plain cookie while a user logging in and then check it inside todo.coffee
But you shouldn't use current_user.id because it's not secure. If you use it then someone might register in the same site and easily listen to other users' private chats by simply providing a random user_id.
Instead use the signed (e.g. Rails-encrypted) cookie as a user's unique broadcast identifier. That way if you register in the same site you would never know some other user's signed cookie - so you can't break into an alien private chat.
app/config/initializers/warden_hooks.rb
See https://rubytutorial.io/actioncable-devise-authentication/
# Be sure to restart your server when you modify this file.
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.delete("#{scope}.id")
end
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{params['user_signed_cookie']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(param_message)
ActionCable.server.broadcast "todo_channel_#{param_message['user_signed_cookie']}", message: 'some message'(not implemented yet)
end
end
todo.coffee
user_signed_cookie = document.cookie.replace(/(?:(?:^|.*;\s*)user.id\s*\=\s*([^;]*).*$)|^.*$/, "$1");
user_logged_in = if user_signed_cookie then true else false
if user_logged_in #avoid repetitive HTTP->WebSockets upgrade pointless browser attempts when no user logged in.
App.todo = App.cable.subscriptions.create {
channel:"TodoChannel"
user_signed_cookie: user_signed_cookie
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: (name, content, _) -> #name, content - message fields
#perform 'notify', name: name, content: content, user_signed_cookie: user_signed_cookie
I have a Rails 5 API with devise_token_auth gem authentications.
Now I want personal chat for authenticated users. I do not have assets as I am using API and front is in native apps and I want native apps messaging.
So how I can authenticate users to use action cable for personal messaging using devise_token_auth gem
No cookies are generally supported in Rails 5 API. See: http://guides.rubyonrails.org/api_app.html#creating-a-new-application .
If you do a common HTTP-authentification at first somewhere at your site ( with devise_token_auth gem), then you get 3 auth headers - access_token, client, uid.
In such case you can use the Basic authentification for your Websockets connection (according https://devcenter.heroku.com/articles/websocket-security#authentication-authorization ) using these 3 auth headers:
Call (I use Chrome Simple WebSocket Client):
ws://localhost:3000/cable/?access-token=ZigtvvcKK7B7rsF_20bGHg&client=TccPxBrirWOO9k5fK4l_NA&uid=client1#example.com
Then process:
# Be sure to restart your server when you modify this file. Action Cable runs in an EventMachine loop that does not support auto reloading.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
params = request.query_parameters()
access_token = params["access-token"]
uid = params["uid"]
client = params["client"]
self.current_user = find_verified_user access_token, uid, client
logger.add_tags 'ActionCable', current_user.email
end
protected
def find_verified_user token, uid, client_id # this checks whether a user is authenticated with devise
user = User.find_by email: uid
# http://www.rubydoc.info/gems/devise_token_auth/0.1.38/DeviseTokenAuth%2FConcerns%2FUser:valid_token%3F
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
This is similar to the common Websockets auth https://rubytutorial.io/actioncable-devise-authentication/
Such authentication is probably enough. I believe it is not necessary additionally to auth at the channel subscription and on every Websocket message sent to server:
http://guides.rubyonrails.org/action_cable_overview.html#server-side-components-connections
For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all the channel subscriptions that are created from there on. The connection itself does not deal with any specific application logic beyond authentication and authorization.
So if your connection is identified_by :current_user, you can later access current_user wherever inside your FooChannel < ApplicationCable::Channel! Example:
class AppearanceChannel < ApplicationCable::Channel
def subscribed
stream_from "appearance_channel"
if current_user
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :on }
current_user.online = true
current_user.save!
end
end
def unsubscribed
if current_user
# Any cleanup needed when channel is unsubscribed
ActionCable.server.broadcast "appearance_channel", { user: current_user.id, online: :off }
current_user.online = false
current_user.save!
end
end
end
PS I you want to use cookies in Rails 5 API, you can switch it on:
http://guides.rubyonrails.org/api_app.html#other-middleware
config/application.rb
config.middleware.use ActionDispatch::Cookies
http://guides.rubyonrails.org/api_app.html#adding-other-modules
controllers/api/application_controller.rb
class Api::ApplicationController < ActionController::API
include ActionController::Cookies
...
Here the auth_headers set in the cookies by ng-token-auth are used to determine if the request is authenticated, if not the connection is refused.
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user # this checks whether a user is authenticated with devise_token_auth
# parse cookies for values necessary for authentication
auth_headers = JSON.parse(cookies['auth_headers'])
uid_name = DeviseTokenAuth.headers_names[:'uid']
access_token_name = DeviseTokenAuth.headers_names[:'access-token']
client_name = DeviseTokenAuth.headers_names[:'client']
uid = auth_headers[uid_name]
token = auth_headers[access_token_name]
client_id = auth_headers[client_name]
user = User.find_by_uid(uid)
if user && user.valid_token?(token, client_id)
user
else
reject_unauthorized_connection
end
end
end
end
I do it this way, since our React Native app sends the authentication inside the headers:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
access_token = request.headers['access-token']
uid = request.headers['uid']
client = request.headers['client']
user = User.find_by_uid(uid)
if user && user.valid_token?(access_token, client)
logger.add_tags 'ActionCable', uid
user
else
reject_unauthorized_connection
end
end
end
end