I have a basic setup with rails + devise + actioncable.
I basically want to send notifications directly and privately to currently signed-in user. My code looks like follows:
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.email
end
private
def find_verified_user
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from current_user
end
end
And it all works just fine. Client get's connected and i can see in the logs he is correctly logged in. Also, i can see a following input in the rails console:
[ActionCable] [admin#example.com] Registered connection (Z2lkOi8vcHJpc21vL1VzZXIvMQ)
[ActionCable] [admin#example.com] NotificationsChannel is transmitting the subscription confirmation
[ActionCable] [admin#example.com] NotificationsChannel is streaming from #<User:0x00007f87f4180b68>
However, when trying to send a notification to this user using a code below, it seems the event is not reaching the user (no errors present!):
2.5.1 :010 > NotificationsChannel.broadcast_to(User.first, test: 'foo')
[ActionCable] Broadcasting to notifications:Z2lkOi8vcHJpc21vL1VzZXIvMQ: {:test=>"pass"}
=> nil
And my javascript consumer logs nothing:
let cable = ActionCable.createConsumer(`ws://mydomain.com/cable`)
let actions = {
received(payload) {
console.log(payload) // <== this line logs nothing!
}
}
cable.subscriptions.create('NotificationsChannel', actions)
Have i done anything wrong in here?
It's likely because you are using stream_from instead of stream_for. When referencing an object (model) rather than a string in your channel, you should use stream_for. Try and do this in notifications_channel.rb:
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
Here is a reference to the documentation: https://api.rubyonrails.org/classes/ActionCable/Channel/Streams.html#method-i-stream_for
Related
I want to authorize connection requests made to the websocket connection via mobile devices.
The code that I have in the Connection < ActionCable::Connection::Base is as follows:
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[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
This is working perfectly for the WebBrowser Clients, as I have set cookies as soon as the user signs in to the system using Devise.
That code looks something like this in the SessionsController from Devise:
class Users::SessionsController < Devise::SessionsController
# POST /resource/sign_in
def create
super
cookies[:user_id] = current_user.id
end
This is working fine.
But when I try to connect to the websocket connection via mobile, i get reject_unauthorized_connection error.
Also I tried setting cookies in the api/signin api, when the user signs in from the mobile. The api code looks something like this:
class Api::SessionsController < Api::ApiController
def create
user = User.find_by_email params[:email]
if user && user.valid_password?(params[:password])
# Set the user cookie
cookies[:user_id] = user.id
render json: { token: token_sign_in(user), user: user, department: user.department, organization: user.organization }
else
render json: { error: "invalid credentials" }, status: :unauthorized
end
end
Is the error happening because you can't set cookies on a mobile application ? If not, then what is the next best approach to authenticate the incoming websocket connection requests from mobile devices without using cookies.
When trying to authenticate connection requests from a mobile device use tokens or a unique identifiers(email id in my case) that the mobile application sends to the server while logging in, with the header.
So, these are the changes that I made in my connection.rb file.
token = request.headers[:email]
if token
#verified_user = User.find_by(email: token)
else
reject_unauthorized_connection
end
You could also use a JWT token instead of an email.
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
In my Ruby on Rails project, I have a user model that is set up by devise. Each user belongs to an account with an account_id.
In my application_controller.rb I have
def set_account
#account = current_user.account
end
This is working fine as in many places of my project I have before_action: set_account and it does assign #account correctly.
When a user signs in, I want the user to subscribe to message_notifications_channel_#{account_id} where account_id is the id of the account that the user belongs to.
This is how I set up 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
When I entered byebug here, User.find_by(id: cookies.encrypted[:user_id]) returned nil and cookies.encrypted[:user_id] is nil too.
This is the setup for message_notifications_channel.rb:
class MessageNotificationsChannel < ApplicationCable::Channel
def subscribed
current_user.appear
# stream_from "some_channel"
stream_from "message_notifications_channel_#{params[:account_id]}"
end
def unsubscribed
current_user.disappear
# Any cleanup needed when channel is unsubscribed
end
end
For message_notifications.coffee, this is the code:
App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: current_user.account_id},
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) ->
# Called when there's incoming data on the websocket for this channel
if data['direction'] == 'incoming'
ding = new Audio('/assets/ding.wav');
ding.play();
$('#conversation-messages').append String(data['message']);
if data['direction'] == 'outgoing'
if $('#message_'+data['id']).length == 0
iphone_sms_sent_sound = new Audio('/assets/iphone_send_sms.mp3');
iphone_sms_sent_sound.play();
$('#conversation-messages').append String(data['message']);
else
$('#message_'+data['id']).replaceWith(data['message']);
I'm using the following to broadcast message in the after_create callback of Message.rb:
ActionCable.server.broadcast "message_notifications_channel_#{self.account_id}", {id: self.id, direction: self.direction, message: ApplicationController.render(partial:'inbox/message', locals: {message: self})}
This would not work and I got "An unauthorized connection attempt was rejected". I tried using App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: #account.id}
This would not work either.
Then I did App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: 3} and commented out current_user.appear and current_user.disappear in message_notifications_channel.rb and everything inside
module ApplicationCable
end
in connection.rb. Then packets will get broadcast and received and things will show up.
How do I get App.message_notifications = App.cable.subscriptions.create {channel: "MessageNotificationsChannel", account_id: }, to use the id of #account.id or current_user.account_id while keeping the methods for user verification in connection.rb?
Thanks!
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 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