graphqlelixirsession-cookiesphoenix-frameworkabsinthe

Absinthe - How to put_session in resolver function?


I'm using Absinthe and have a sign in mutation. When users send over valid credentials, I'd like to set a session cookie in the response via put_session.

The problem I'm facing is that I'm not able to access the conn from within a resolver function. That tells me that I'm not supposed to update the connection's properties from within a resolver.

Is it possible to do this with Absinthe? What are some alternative solutions?


Solution

  • It looks like one solution is:

    1. In the resolver, resolve either an {:ok, _} or an {:error, _} as normal
    2. Add middleware after the resolver to pattern match that resolution.value returned from step 1 and update the GraphQL context
    3. Use the before_send feature of Absinthe (which has access to both the GraphQL context and the connection to put_session before sending a response

    Code Example

    Mutation:

    mutation do
      @desc "Authenticate a user."
      field :login, :user do
        arg(:email, non_null(:string))
        arg(:password, non_null(:string))
        resolve(&Resolvers.Accounts.signin/3)
    
        middleware(fn resolution, _ ->
          case resolution.value do
            %{user: user, auth_token: auth_token} ->
              Map.update!(
                resolution,
                :context,
                &Map.merge(&1, %{auth_token: auth_token, user: user})
              )
    
            _ ->
              resolution
          end
        end)
      end
    end
    

    Resolver:

    defmodule AppWeb.Resolvers.Accounts do
      alias App.Accounts
    
      def signin(_, %{email: email, password: password}, _) do
        if user = Accounts.get_user_by_email_and_password(email, password) do
          auth_token = Accounts.generate_user_session_token(user)
          {:ok, %{user: user, auth_token: auth_token}}
        else
          {:error, "Invalid credentials."}
        end
      end
    end
    

    Router:

    defmodule AppWeb.Router do
      use AppWeb, :router
    
      pipeline :api do
        plug(:accepts, ["json"])
        plug(:fetch_session)
      end
    
      scope "/" do
        pipe_through(:api)
    
        forward("/api", Absinthe.Plug,
          schema: AppWeb.Schema,
          before_send: {__MODULE__, :absinthe_before_send}
        )
    
        forward("/graphiql", Absinthe.Plug.GraphiQL,
          schema: AppWeb.Schema,
          before_send: {__MODULE__, :absinthe_before_send}
        )
      end
    
      def absinthe_before_send(conn, %Absinthe.Blueprint{} = blueprint) do
        if auth_token = blueprint.execution.context[:auth_token] do
          put_session(conn, :auth_token, auth_token)
        else
          conn
        end
      end
    
      def absinthe_before_send(conn, _) do
        conn
      end
    end