Hello all,
I am trying to build my first GraphQL subscription using Absinthe. I got as far as making the subscription itself using the authenticated user socket (with Guardian), but I am a bit lost on how to make a “per-user” channel that only a logged in user subscribes to.
The subscription itself is simple and it’s purpose it’s getting a download link once it’s ready from a background job:
# in schema.ex
subscription do
field :download_ready, :download_link do
config(fn _args, %{context: ctx} ->
{:ok, topic: "user_download"}
end)
end
end
If I leave topic as “user_download” I have no problem in subscribing, I can of course pass the user ID as argument and append it to the topic: user_download:#{args[:id]}
.
However, I think passing it as argument is wrong, since an authenticated socket is used and already carry this information. The client subscription should ideally stay as simple as:
subscription {
downloadReady {
url
}
}
So that particular user is automatically subscribed and getting only his/her notifications.
Unfortunately I don’t have access to the signed socket from the first mentioned field :download_ready
block. The context there is not a socket context.
Maybe I am thinking about this wrong. How do you tackle the need of private per-user subscriptions/channels?
Hey @strzibny, in your particular case could you do:
config(fn _args, %{context: ctx} ->
{:ok, topic: "user:#{ctx.current_user.id}"}
end)
Then the actual topic of the subscription downloadReady
will be per user, without having to pass it in as an argument.
3 Likes
The trouble I am having is that the context won’t have the current_user or anything I set on the signed socket. I tried to explain it, but perhaps failed to be more clear. The way you suggest is exactly the way I tried and expected it to work.
Can you show what you have in your socket connect function?
Yes, it’s pretty much standard (I commented out passing current login as struct, but neither getting id or passing this works for me):
defmodule MyWeb.LoginSocket do
use Phoenix.Socket
use Absinthe.Phoenix.Socket,
schema: MyWeb.Private.Schema
def connect(%{"Authorization" => header_content}, socket) do
[[_, token]] = Regex.scan(~r/^Bearer (.*)/, header_content)
case Guardian.Phoenix.Socket.authenticate(socket, MyWeb.Public.Guardian, token) do
{:ok, authed_socket} ->
# socket =
# Absinthe.Phoenix.Socket.put_options(socket,
# context: %{
# current_login: "test"
# }
# )
{:ok, authed_socket}
{:error, _} ->
:error
end
end
# This function will be called when there was no authentication information
def connect(_params, socket) do
:error
end
def id(_socket), do: nil
end
Note that authentication happens and I get correct credentials in authed_socket.assigns.guardian_default_claims["sub"]
. It’s just not what I get on the other side in Absinth GraphQL definitions.
1 Like
So put_opts
has to be used.
def connect(%{"Authorization" => header_content}, socket) do
[[_, token]] = Regex.scan(~r/^Bearer (.*)/, header_content)
case Guardian.Phoenix.Socket.authenticate(socket, DigiWeb.Public.Guardian, token) do
{:ok, authed_socket} ->
login_id = authed_socket.assigns.guardian_default_claims["sub"]
new_socket =
Absinthe.Phoenix.Socket.put_opts(authed_socket,
context: %{
current_login: login_id
}
)
{:ok, new_socket}
{:error, _} ->
:error
end
end
You then need to access it as context[:context][:current_login]
2 Likes
I also tried to write a bit more about this here http://nts.strzibny.name/graphql-subscriptions-with-elixir-and-absinth/ if anybody come across this.
1 Like