Using websocket-rails, I can use the
following code in my controller to tricker a websocket publish event:
WebsocketRails[:channel_name].trigger('event_name', { foo: "bar" }.to_json)
My goal is to create a pub-sub channel for the current user only. Take for example
the event "new chat message sent". I want to push this event only the receiver's channel.
I'm currently making a unique channel name for each user based on their ID.
I put the following code in my controller (having defined a current_user
method elsewhere:
WebsocketRails[:"user#{current_user.id}"].trigger("event_name", { foo: "bar" }.to_json)
And then in my Javascript, I subscribe the current user to their own channel with the following:
<% if #current_user %>
var dispatcher = new WebSocketRails('localhost:3000/websocket');
channel = dispatcher.subscribe('user<%= #current_user.id %>');
channel.bind('event_name', function(data) {
console.log(data)
});
<% end %>
The gist of it is using string interpolation to make a new channel for each user, i.e.
user12 and user123 channels.
The problem is this is not really secure. Any user can access anyone else's
private channel just by pasting some Javascript in. For example, if user #1 wants
to access user #2's news feed, they could just type dispatcher.subscribe('user2').
How would you solve this issue? Is there another pub-sub library which has this feature
built into it?
Looking on WebsocketRails' wiki entry on the subject,
I tried adding the follolwing code to config/initializers/websockets.rb
WebsocketRails::EventMap.describe do
namespace :websocket_rails do
subscribe :subscribe_private, to: ConnectionsController, with_method: :authorize_channels
end
end
And the following to app/controllers/connections_controller.rb
class ConnectionsController < WebsocketRails::BaseController
def authorize_channels
channel_name = WebsocketRails[message[:channel]]
current_user = User.find_by(id: session["current_user_id"])
if current_user && "user#{current_user.id}".eql?(channel_name)
accept_channel current_user
else
deny_channel({ message: "auth failed" })
end
end
end
And then elsewhere, I'm calling WebsocketRails[:"user#{#current_user.id}"].make_private
This doesn't seem to have any effect though.
Related
Making the title for this question was extremely difficult, but essentially, I have a service object in my Rails app that creates a flow for Resume Processing. I am trying to use ActionCable to connect my frontend with my backend. The current way I do this is by instantiating my Service Object in my controller:
def create_from_resume
...
ResumeParseService.new(#candidate, current_user)
end
My Service then begins by broadcasting to my front end to open the corresponding modal:
Service:
class ResumeParseService
attr_reader :user
attr_reader :employee
attr_reader :candidate
def initialize(candidate, user)
#user = user
#employee = user.employee
#candidate = candidate
#progress = 0
--> broadcast_begin
end
def begin_from_parse_modal
broadcast_progress(10)
parsed_resume = get_a_resume_while_hiding_implementation_details
broadcast_progress(rand(40..60))
...
broadcast_progress(100 - #progress)
...
end
private
def broadcast_begin
ResumeParseChannel.broadcast_and_set_service(self, user, {
event_name: 'transition_screen',
props: {
to: 'parse',
},
})
end
def broadcast_progress(addition)
#progress += addition
ResumeParseChannel.broadcast_to(user, {
event_name: 'progress',
props: {
progress: #progress,
},
})
end
def broadcast_transition_screen(screen_name, body = nil)
ResumeParseChannel.broadcast_to(user, {
event_name: 'transition_screen',
props: {
to: screen_name,
data: body,
},
})
end
end
Rails Channel:
# frozen_string_literal: true
class ResumeParseChannel < ApplicationCable::Channel
def subscribed
stream_for(current_user)
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def self.broadcast_and_set_service(service, *args)
#service = service
broadcast_to *args
end
def screen_transitioned(data)
case data['screen_name']
when 'parse'
pp #service
#service.begin_from_parse_modal
else
# type code here
end
end
private
def current_user
if (current_user = env["warden"].user)
current_user
else
reject_unauthorized_connection
end
end
end
Which my channel then takes care of. Later, my channel will send back a 'progress update' to let my service know the modal opened successfully:
JS Channel:
consumer.subscriptions.create(
{ channel: "ResumeParseChannel" },
{
connected() {
document.addEventListener("resume-parse:screen_transitioned", event =>
--> this.perform("screen_transitioned", event.detail)
);
},
}
);
Now, my problem is that once that message gets sent back to my (ruby) channel, I can't think of a way for it to find my existing instance of my service object and use it. As you can see, I tried to set an instance var on the channel with the service object instance on the first broadcast, but that (and a million other things) did not work. I need to call #begin_from_parse_modal once I get the 'screen_transitioned' with the screen_name of 'parse'. Ideally, I'd like to separate the broadcasting logic and the parsing logic as much as possible.
I understand that the instance of the channel can be thought of as the actual subscription, but I just don't understand what the best practice is of a system where I can send a "do this" message, and then do something once I get a "its been done" message.
Please let me know if I missed anything in terms of explanation and/or code. Feel free to also let me know if I should do something differently next time I ask something! This is my first time asking on stackoverflow, but it's about my billionth time looking for an answer :)
edit: I'm still dumbfounded by this seemingly common scenario. Could it possibly be best practice to just simply have the channel act as the service object? If so, how would we store state on it? The only possible way I can think of this working in any form is to send the full state in each WS message. Or at least the id's to each record thats in state and then lookup each record on each message. This seems unreasonably complex and expensive. I have scoured other questions and even ActionCable tutorials to find anyone using a service object with receiving messages and have found nothing. SOS!
I have created one project where I have one customer and another contractor. I implemented ruby on rails actioncable for chat. All it is going good but issue is coming when two different people chat with one person, that person is receiving both messages in socket window. I realised that I have setup conversation-#{user_id} as a channel, so user is listening on this channel now two people send chat to him, they both will come on same channel. How can I avoid this? or can I add another user in channel string, but I found it is very difficult. Any idea where I have to send params to subscribe method.
connection
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_session_user
end
def find_session_user
current_user = User.find_by(id: cookies.signed[:actioncable_user_id])
current_user || reject_unauthorized_connection
end
end
My conversation channel
class ConversationChannel < ApplicationCable::Channel
def subscribed
stream_from "conversations-#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
def speak(data)
message_params = data["message"].each_with_object({}) do |el, hash|
hash[el.values.first] = el.values.last
end
end
ActionCable.server.broadcast(
"conversations-#{current_user.id}",
message: message_params,
)
end
This code is just condense version, but as it will start conversation-user_id as a channel, so definitely when it is connected and other people send message, this user will receive them in same socket. so I have to do like `conversation-user_id-anotehr_user. Right now it is working on web/mobile and all good, but when two user communicate with one user it create issue by displaying two users chat on one socket.
Any idea how can I create such channel.
I have solved this issue by binding chat with 2 persons and I have another requirement of job specific chats, so have bound it with it too. Now my conversation channel is like conversation-(talk_id)-(listern_id)-job_id so it all good now. Following are changes I did
1. I removed channel file from assets/javascript as it is automatically load on my application, but I want to bound it with few parameters so I added it to specific view. My controller has already few parameters so I have changed this javascript to following
<script>
App.conversation = App.cable.subscriptions.create({channel: "ConversationChannel",
job_id: <%= #job.id %>,
contractor: <%= #contractor %>
}, {
connected: function() {},
disconnected: function() {},
received: function(data) {
var conversation = $('#conversations-list').find("[data-conversation-id='" + data['conversation_id'] + "']");
conversation.find('.messages-list').find('ul').append(data['message']);
var messages_list = conversation.find('.messages-list');
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
},
speak: function(message) {
return this.perform('speak', {
message: message
});
}
});
Now when connection establish it sends both parameters and channel channel properly individual. On my conversation.rb I have just minor change
def subscribed
stream_from "conversations-#{current_user.id}-#{params[:contractor]}-#{params[:job_id]}"
end
Now everything working perfectly as per our requirements, each channel is being made with 2 users+job Id so I they can communicate on specific job and with specific users, so there no more other person can listen other conversation.
Just posting may help someone.
How can I create a dynamic QR code on a rails app such that the moment it is scanned and successfully processed, the open page bearing the QR code can then just redirect to the success page.
This is similar to the whatsapp web implementation where the moment the android app scans the QR code, the page loads the messages.
Am more interested in is the management of the sessions. When the QR is scanned am able to reload the page where it was displayed and then redirect to another page. any idea?
You could update the User model to be able to store an unique token value to use in you QR Codes; e.g.
$ rails generate migration add_token_to_user token:string
Or a separate related model
$ rails generate model Token value:string user:belongs_to
Then generate unique Token value that can be used within an URL and encode it
into a QRCode
# Gemfile
gem "rqrcode"
# app/models/token.rb
require "securerandom"
class User < ActiveRecord::Base
def generate_token
begin
self.token = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(token: token)
end
def qr_code
RQRCode::QRCode.new(
Rails.application.routes.url_helpers.url_for(
controller: "session",
action: "create",
email: email,
token: token
)
)
end
end
Then display this QRCode somewhere in your application
# app/views/somewhere.html.erb
<%= #token.qr_code.as_html %>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/login", to: "sessions#new"
end
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email], token: params[:token])
if user
session[:user_id] = user.id # login user
user.update(token: nil) # nullify token, so it cannot be reused
redirect_to user
else
redirect_to root_path
end
end
end
References:
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
I am adding a new answer for two reasons:
1. Acacia repharse the question with an emphasis on What's App redirection of
the page with the QR Code being view, which I did not address in my initial
solution due a misunderstanding of the problem, and
2. Some people have found the first answer helpful and this new answer would
change it significantly that whilst similar, but no longer the same
When the QR is scanned am able to reload the page where it was displayed and
then redirect to another page
-- Acacia
In order to achieve this there requires to some kind of open connection on the
page that is displaying the QRCode that something interpretting said QRCode can
use to effect it. However, because of the application you trying to mimic
requires that only that one User viewing the page is effected, whilst not
actually being logged in yet, would require something in the page to be unique.
For the solution to this problem you will need a couple of things:
An unique token to identify the not logged-in User can use to be contacted /
influenced by an external browser
A way of logging in using JavaScript, in order to update the viewed page to
be logged after previous step's event
Some kind of authentication Token that can be exchange between the
application and the external QRCode scanner application, in order to
authentication themselves as a specific User
The following solution stubs out the above 3rd step since this is to
demonstrate the idea and is primarily focused on the server-side of the
application. That being said, the solution to the 3rd step should be as simple
as passing the know User authentication token by appending it to the URL within
the QRCode as an additional paramater (and submitting it as a POST request,
rather than as a GET request in this demonstration).
You will need some random Tokens to use to authentication the User with and
exchange via URL embedded within the QCcode; e.g.
$ rails generate model Token type:string value:string user:belongs_to
type is a reserverd keyword within Rails, used for Single Table Inheritance.
It will be used to specific different kinds of / specialized Tokens within this
application.
To generate unique Token value that can be used within an URL and encode it
into a QRCode, use something like the following model(s) and code:
# Gemfile
gem "rqrcode" # QRCode generation
# app/models/token.rb
require "securerandom" # used for random token value generation
class Token < ApplicationRecord
before_create :generate_token_value
belongs_to :user
def generate_token_value
begin
self.value = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
end while self.class.exists?(value: value)
end
def qr_code(room_id)
RQRCode::QRCode.new(consume_url(room_id))
end
def consume_url(room_id)
Rails.application.routes.url_helpers.url_for(
host: "localhost:3000",
controller: "tokens",
action: "consume",
user_token: value,
room_id: room_id
)
end
end
# app/models/external_token.rb
class ExternalToken < Token; end
# app/models/internal_token.rb
class InternalToken < Token; end
InternalTokens will be only used within the application itself, and are
short-lived
ExternalTokens will be only used to interact with the application from
outside; like your purposed mobile QRCode scanner application; where the User
has either previously registered themselves or has logged in to allow for
this authentication token to be generated and stored within the external app
Then display this QRCode somewhere in your application
# e.g. app/views/tokens/show.html.erb
<%= #external_token.qr_code(#room_id).as_html.html_safe %>
I also hide the current #room_id within the <head> tags of the application
using the following:
# e.g. app/views/tokens/show.html.erb
<%= content_for :head, #room_id.html_safe %>
# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>QrcodeApp</title>
<!-- ... -->
<%= tag("meta", name: "room-id", content: content_for(:head)) %>
<!-- ... -->
</head>
<body>
<%= yield %>
</body>
</html>
Then wire up your application's routes and controllers to process that generated
and encoded QRCode URL.
For Routes we need:
Route to present the QRCode tokens; "token#show"
Route to consume / process the QRCode tokens; "token#consume"
Route to log the User in with, over AJAX; "sessions#create"
We will also need some way of opening a connection within the display Token page
that can be interacted with to force it to login, for that we will need:
mount ActionCable.server => "/cable"
This will require Rails 5 and ActionCable to implment, otherwise another
Pub/Sub solution; like Faye; will need to be used instead with older versions.
All together the routes look kind of like this:
# config/routes.rb
Rails.application.routes.draw do
# ...
# Serve websocket cable requests in-process
mount ActionCable.server => "/cable"
get "/token-login", to: "tokens#consume"
post "/login", to: "sessions#create"
get "/logout", to: "sessions#destroy"
get "welcome", to: "welcome#show"
root "tokens#show"
end
Then Controllers for those actions are as follows:
# app/controller/tokens_controller.rb
class TokensController < ApplicationController
def show
# Ignore this, its just randomly, grabbing an User for their Token. You
# would handle this in the mobile application the User is logged into
session[:user_id] = User.all.sample.id
#user = User.find(session[:user_id])
# #user_token = Token.create(type: "ExternalToken", user: #user)
#user_token = ExternalToken.create(user: #user)
# keep this line
#room_id = SecureRandom.urlsafe_base64
end
def consume
room_id = params[:room_id]
user_token = params[:user_token] # This will come from the Mobile App
if user_token && room_id
# user = Token.find_by(type: "ExternalToken", value: user_token).user
# password_token = Token.create(type: "InternalToken", user_id: user.id)
user = ExternalToken.find_by(value: user_token).user
password_token = InternalToken.create(user: user)
# The `user.password_token` is another random token that only the
# application knows about and will be re-submitted back to the application
# to confirm the login for that user in the open room session
ActionCable.server.broadcast("token_logins_#{room_id}",
user_email: user.email,
user_password_token: password_token.value)
head :ok
else
redirect_to "tokens#show"
end
end
end
The Tokens Controller show action primarily generates the #room_id value for
reuse in the view templates. The rest of the code in the show is just used to
demonstrate this kind of application.
The Tokens Controller consume action requires a room_id and user_token to
proceed, otherwise redirects the User back to QRCode sign in page. When they are
provided it then generates an InternalToken that is associated with the User
of the ExternalToken that it will then use to push a notification / event to
all rooms with said room_id (where there is only one that is unique to the
User viewing the QRCode page that generate this URL) whilst providing the
necessary authentication information for a User (or in this case our
application) to log into the application without a password, by quickly
generating an InternalToken to use instead.
You could also pass in the User e-mail as param if the external application
knows about it, rather than assuming its correct in this demonstration example.
For the Sessions Controller, as follows:
# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:user_email])
internal_token = InternalToken.find_by(value: params[:user_password_token])
# Token.find_by(type: "InternalToken", value: params[:user_password_token])
if internal_token.user == user
session[:user_id] = user.id # login user
# nullify token, so it cannot be reused
internal_token.destroy
# reset User internal application password (maybe)
# user.update(password_token: SecureRandom.urlsafe_base64)
respond_to do |format|
format.json { render json: { success: true, url: welcome_url } }
format.html { redirect_to welcome_url }
end
else
redirect_to root_path
end
end
def destroy
session.delete(:user_id)
session[:user_id] = nil
#current_user = nil
redirect_to root_path
end
end
This Sessions Controller takes in the user_email and user_password_token to
make sure that these two match the same User internally before proceeding to
login. Then creates the user session with session[:user_id] and destroys the
internal_token, since it was a one time use only and is only used internally
within the application for this kind of authentication.
As well as, some kind of Welcome Controller for the Sessions create action to
redirect to after logging in
# app/controller/welcome_controller.rb
class WelcomeController < ApplicationController
def show
#user = current_user
redirect_to root_path unless current_user
end
private
def current_user
#current_user ||= User.find(session[:user_id])
end
end
Since this aplication uses
ActionCable, we
have already mounted the /cable path, now we need to setup a Channel that is
unique to a given User. However, since the User is not logged in yet, we use the
room_id value that was previously generated by the Tokens Controller show
action since its random and unique.
# app/channels/tokens_channel.rb
# Subscribe to `"tokens"` channel
class TokensChannel < ApplicationCable::Channel
def subscribed
stream_from "token_logins_#{params[:room_id]}" if params[:room_id]
end
end
That room_id was also embedded within the <head> (although it could a hidden
<div> element or the id attribtue of the QRCode, its up to you), which means
it can be pulled out to use in our JavaScript for receiving incoming boardcasts
to that room/QRCode; e.g.
// app/assets/javascripts/channels/tokens.js
var el = document.querySelectorAll('meta[name="room-id"]')[0];
var roomID = el.getAttribute('content');
App.tokens = App.cable.subscriptions.create(
{ channel: 'TokensChannel', room_id: roomID }, {
received: function(data) {
this.loginUser(data);
},
loginUser: function(data) {
var userEmail = data.user_email;
var userPasswordToken = data.user_password_token; // Mobile App's User token
var userData = {
user_email: userEmail,
user_password_token: userPasswordToken
};
// `csrf_meta_tags` value
var el = document.querySelectorAll('meta[name="csrf-token"]')[0];
var csrfToken = el.getAttribute('content');
var xmlhttp = new XMLHttpRequest();
// Handle POST response on `onreadystatechange` callback
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE ) {
if (xmlhttp.status == 200) {
var response = JSON.parse(xmlhttp.response)
App.cable.subscriptions.remove({ channel: "TokensChannel",
room_id: roomID });
window.location.replace(response.url); // Redirect the current view
}
else if (xmlhttp.status == 400) {
alert('There was an error 400');
}
else {
alert('something else other than 200 was returned');
}
}
};
// Make User login POST request
xmlhttp.open(
"POST",
"<%= Rails.application.routes.url_helpers.url_for(
host: "localhost:3000", controller: "sessions", action: "create"
) %>",
true
);
// Add necessary headers (like `csrf_meta_tags`) before sending POST request
xmlhttp.setRequestHeader('X-CSRF-Token', csrfToken);
xmlhttp.setRequestHeader("Content-Type", "application/json");
xmlhttp.send(JSON.stringify(userData));
}
});
Really there is only two actions in this ActionCable subscription;
received required by ActionCable to handle incoming requests/events, and
loginUser our custom function
loginUser does the following:
Handles incoming data to build a new data object userData to POST back to
our application, which contains User information; user_email &
user_password_token; required to login over AJAX using an authentication
Token as the password (since its somewhat insecure, and passwords are usually
hashed; meaning that they unknown since they cannot be reversed)
Creates a new XMLHttpRequest() object to POST without jQuery, that sends a
POST request at the JSON login URL with the userData as login information,
whilst also appending the current HTML page CSRF token; e.g.
Otherwise the JSON request would fail without it
The xmlhttp.onreadystatechange callback function that is executed on a
response back from the xmlhttp.send(...) function call. It will unsubscribe
the User from the current room, since it is no longer needed, and redirect the
current page to the "Welcomw page" it received back in its response. Otherwise
it alerts the User something failed or went wrong
This will produce the following kind of application
You can access a copy of the project I worked on at the following URL:
Sonna/remote-url_qrcode-signin: ruby on rails - How can I implement Whatsapp life QR code authentication - Stack Overflow
The only this solution does not address is the rolling room token generation,
which would either require either a JavaScript library to generate/regenerate
the URL with the Room Token or a Controller Action that return a regenerated
QRCode as either image or HTML that can be immediately displayed within the
page. Either method still requires you to have some JavaScript that closes the
current connection and opens a new one with a new room/session token that can
used so that only it can receive mesages from, after a certain amount of time.
References:
Action Cable Overview — Ruby on Rails Guides
whomwah/rqrcode: A Ruby library that encodes QR Codes
Module: SecureRandom (Ruby 2_2_1)
#352 Securing an API - RailsCasts
I've got an app where users submit weeks which can be approved or denied, and in my weeks controller I have the following lines meant to iterate over the selected weeks, find their corresponding users and send each user an email:
elsif params[:commit] == "Reject selected weeks"
user_week = Week.where(id: params[:weeks_ids])
user_week.update_all(approved?: false)
# fetch the set of user_emails by converting the user_weeks to user_ids
users = User.find(user_week.pluck(:user_id))
users.each do |user|
#iterate over the users and send each one an email
UserMailer.send_rejection(user).deliver
end
flash[:info] = "Selected weeks were Rejected."
end
redirect_to weeks_path
When I attempt to reject a week, I receive the following error message:
undefined method `send_rejection' for UserMailer:Class
I'm adding on to pre-existing code and have little knowledge of MVC, so the only issues I can think of would be with placing the mailer method in the wrong file or sending an incorrect type of arg to the mailer method.
Here is "send_rejection", the mailer contained in my user model.
def send_rejection(user)
UserMailer.reject_timesheet(user).deliver_now
end
The corresponding method in my user_mailer.rb file:
def reject_timesheet(user)
#greeting = "Hi"
mail to: user.email, subject: "Rejected Timesheet"
end
New to rails and not sure where I'm going wrong.
This is not a problem of MVC, one question I'd probably ask is why are you not calling the reject_timesheet directly instead of send_rejection.
You're getting the error because as you said the method is defined in the user model, so in order to call the method, you'd need to do:
user.send_rejection
In which case I doubt you'd be needing to pass a user argument to the send_rejection, as you could just do:
class User
def send_rejection
UserMailer.reject_timesheet(self).deliver_now
end
end
then in your controller:
...
users.each do |user|
#iterate over the users and send each one an email
user.send_rejection
end
...
I believe you could also clean up your codebase a bit and possibly refactor some logic, but basically this approach should resolve your errors.
Let me know if that helps
I am developing web application on rails4.
I have user, block and request tables.
Block has blocker_id and blocked_id.
Some users have requests and some users can block other users.
I want to get some request entity that is created by some user and not blocked another user.
Assume there is r1 that is created by u1.
u2 wants to get r1 if u2 not blocks u1.
like Request.where("requests.user_id not blocked by u2")
Without knowing the relations you have set up for the 3 models, it's a little hard to give a complete response. The following is one scenario.
You're trying to determine whether or not to display a request from a user.
This depends on whether or not the logged in User (lets call them current_user) has blocked the requesting user (lets call them requestor). A blocked user can be found in the Blocked table.
You could first query the Block table to see if the user has been blocked:
Block.find_by(blocked_id: requestor.id, blocker_id: current_user.id)
That will return a Block object if one is found, otherwise it will return nil. Setting that to a variable will now give a user's blocked status. Probably best set in the controller action.
class RequestsController << ApplicationController
def show
#blocked_status = Block.find_by(blocked_id: requestor.id, blocker_id: current_user.id)
#request = Request.find_by(request_from_id: requestor.id, request_to_id: current_user.id)
end
end
Then you could use that information in your view to either display the request or not
in requests.html.erb
<% unless #blocked_status %>
# html to display request from requestor to current_user
<% end %>
Or
use a scope to find the request with something like:
scope :not_blocked_by, -> (user_id) {
joins("JOIN `blocks` b ON b.`blocked_id` = `requests`.`request_from_id` AND b.`blocker_id` = #{current_user.id}")
.where.not('b.blocked_id = :user_id', user_id: user_id)
}
then in the controller.
class RequestsController << ApplicationController
def show
#request = Request.not_blocked_by(#requestor.id)
end
end