Pow: Use Pow.PasswordReset.Plug.create_reset_token without a conn

Created on 4 Jan 2019  路  10Comments  路  Source: danschultzer/pow

I have a site (https://github.com/axelclark/golf) that has both an html frontend and a graphql api for a mobile app. I have the ResetPassword extension set up for html. I'm trying to enable a user to send the reset token email from their mobile app using graphql.

I'm trying to figure out which Pow.PasswordReset functions to use in my graphql resolver which acts like a controller. The problem is I don't have a conn with the config in it. I just have the user email from the params.

  def create_reset_token(_parent, %{email: email}, _resolution) do
    # Need to create the token and send the email
  end

Any suggestions on how to do this? I only want to enable the create action through graphql.

Most helpful comment

Figured it out, but leaving for the record:
Pow.Plug.put_config(plug: Pow.Plug.Session, otp_app: :kyodo_web)

I found this closed issue when trying to wire up login/registration via absinthe. While this is super helpful, I am running into problems when attempting to use this method. Is there something special about session/registrations, by any chance?

        %Plug.Conn{}
        |> Pow.Plug.put_config(otp_app: :kyodo_web)
        |> IO.inspect()
        |> Pow.Plug.authenticate_user(%{"email" => input.email, "password" => input.password})

Results in:
# Pow plug was not found in config. Please use a Pow plug that puts the:plugin the Pow configuration.

I'm aiming to move all of this functionality into Absinthe and then I'd be into making a pow-absinthe package if things go well... Either way, maybe I can help w/ the guides if there are some good learnings.

UPDATE: In digging through the tests it seems this has to do w/ not properly setting up the session in the conn. I will keep digging but this is starting to get hairy for me :)

All 10 comments

Here is my current approach which seems to be working. I send the email address back regardless of whether the email is sent.

defmodule GolfWeb.Resolvers.Accounts do
  alias GolfWeb.Router.Helpers, as: Routes
  alias PowResetPassword.Phoenix.Mailer

  def create_reset_token(_parent, %{email: email_address}, _resolution) do
    config = Application.get_env(:golf, :pow, [])
    conn = Pow.Plug.put_config(%Plug.Conn{}, config)
    params = %{"email" => email_address}

    conn
    |> PowResetPassword.Plug.create_reset_token(params)
    |> respond_create(email_address)
  end

  defp respond_create({:ok, %{token: token, user: user}, conn}, email_address) do
    url =
      Routes.pow_reset_password_reset_password_url(
        GolfWeb.Endpoint,
        :edit,
        token
      )

    email = Mailer.reset_password(conn, user, url)
    Pow.Phoenix.Mailer.deliver(conn, email)

    {:ok, %{email: email_address}}
  end

  defp respond_create({:error, _, _}, email_address) do
    {:ok, %{email: email_address}}
  end
end

The plug modules handles plug connections, so nearly all plug methods will be focused on conn transformations. This also includes using configuration from the connection which could be set in the endpoint rather than env config. With absinthe plug all of that will be bypassed. I've not worked with GraphQL/absinthe before, but I'll take a look at the absinthe and absinthe plug libraries to see if there is a cleaner way of handling this. It would be ideal if the connection could be passed along, but I'll also take a look at the API in Pow and see if it makes sense to separate some of the reset password method logic to make it easier for this type of customization.

But your approach looks great. Instead of loading the app env you could set it to load the :otp_app config the same way as in your endpoint:

    params = %{"email" => email_address}

    %Plug.Conn{}
    |> Pow.Plug.put_config(otp_app: :golf)
    |> PowResetPassword.Plug.create_reset_token(params)
    |> respond_create(email_address)

Ok, I have given it some thought and your solution is probably the best one. Since this logic is spread over a controller and plug module, I don't think there's much that can be done to make a simpler integration.

I would probably try write some of these bits into smaller methods so refactoring will be easier if there'll be any breaking changes in Pow in the future:

defmodule GolfWeb.Resolvers.Accounts do
  alias GolfWeb.Router.Helpers, as: Routes

  def create_reset_token(_parent, %{email: email_address}, _resolution) do
    conn()
    |> PowResetPassword.Plug.create_reset_token(%{email: email_address})
    |> maybe_send_email(email_address)
  end

  defp maybe_send_email({:ok, %{token: token, user: user}, conn}, email_address) do
    deliver_email(conn, user, token)

    {:ok, %{email: email_address}}
  end
  defp maybe_send_email({:error, _, _}, email_address) do
    {:ok, %{email: email_address}}
  end

  defp conn(), do: Pow.Plug.put_config(%Plug.Conn{}, otp_app: :golf)

  defp deliver_email(conn, user, token) do
    url = reset_password_url(token)
    email = PowResetPassword.Phoenix.Mailer.reset_password(conn, user, url)

    Pow.Phoenix.Mailer.deliver(conn, email)
  end

  defp reset_password_url(token) do
    Routes.pow_reset_password_reset_password_url(
      GolfWeb.Endpoint,
      :edit,
      token
    )
  end
end

This is all about your preference though, the way you have written it is good as it is 馃槃

I got another idea for something that can make the integration cleaner.

First we'll pass the conn to the context:

  pipeline :graphql do
    plug MyAppWeb.Plugs.AbsintheContext
  end
defmodule MyAppWeb.Plugs.AbsintheContext do
  import Plug.Conn

  def init(default), do: default

  def call(conn, _default), do: Absinthe.Plug.put_options(conn, context: %{conn: conn})
end

Then we'll remove render handling in the resolvers as it should only return data:

defmodule GolfWeb.Resolvers.Accounts do
  def create_reset_token(_parent, %{email: email_address}, _resolution) do
    email_address
    |> Context.get_by_email(otp_app: :golf)
    |> maybe_generate_token()
  end

  defp maybe_generate_token(nil) do
    # error response
  end
  defp maybe_generate_token(user) do
    uuid = Pow.UUID.generate()
    backend = Pow.Config.get(otp_app: :golf, :cache_store_backend, Pow.Store.Backend.EtsCache)
    PowResetPassword.Store.ResetTokenCache.put([backend: backend], uuid, user)

    {:ok, %{user: user, token: token}}
  end
end

And finally handle the mail delivery in the response layer (like controller):

    @desc "Resets a password"
    field :reset_password, :user_email do
      arg(:email, non_null(:string))
      resolve(&Resolvers.Accounts.create_reset_token/3)
      middleware &send_reset_password_email/2
    end

    def send_reset_password_email(%{value: %{user: user, token: token}, context: %{conn: conn}} = resolution, _) do
      url = Routes.pow_reset_password_reset_password_url(
        GolfWeb.Endpoint,
        :edit,
        token
      )
      email = PowResetPassword.Phoenix.Mailer.reset_password(conn, user, url)

      Pow.Phoenix.Mailer.deliver(conn, email)

      %{resolution | value: %{email: user.email}}
    end

Figured it out, but leaving for the record:
Pow.Plug.put_config(plug: Pow.Plug.Session, otp_app: :kyodo_web)

I found this closed issue when trying to wire up login/registration via absinthe. While this is super helpful, I am running into problems when attempting to use this method. Is there something special about session/registrations, by any chance?

        %Plug.Conn{}
        |> Pow.Plug.put_config(otp_app: :kyodo_web)
        |> IO.inspect()
        |> Pow.Plug.authenticate_user(%{"email" => input.email, "password" => input.password})

Results in:
# Pow plug was not found in config. Please use a Pow plug that puts the:plugin the Pow configuration.

I'm aiming to move all of this functionality into Absinthe and then I'd be into making a pow-absinthe package if things go well... Either way, maybe I can help w/ the guides if there are some good learnings.

UPDATE: In digging through the tests it seems this has to do w/ not properly setting up the session in the conn. I will keep digging but this is starting to get hairy for me :)

In case this helps someone, I was trying to figure out how to deliver an invitation mail from a LiveComponent, and this issue put me on the right track i.e.:

  defp deliver_invitation_email(socket, user, invited_by) do
    conn = conn(socket)
    token = PowInvitation.Plug.sign_invitation_token(conn, user)
    url = Routes.pow_invitation_invitation_url(conn, :show, token)
    email = Mailer.invitation(conn, user, invited_by, url)

    Pow.Phoenix.Mailer.deliver(conn, email)
  end

  defp conn(%{endpoint: endpoint} = _socket) do
    Conn
    |> struct!(%{
      private: %{phoenix_endpoint: endpoint},
      secret_key_base: endpoint.config(:secret_key_base)
    })
    |> Pow.Plug.put_config(otp_app: :my_app)
  end

I just want to note, that it isn't proper to pass conninto absinthe context and rely on it because GraphQL API is transport agnostic. In my opinion building new conn inside a resolver is the best workaround nowadays.

This worked for me, though this line

defp conn(), do: Pow.Plug.put_config(%Plug.Conn{}, otp_app: :golf)

is giving me dialyzer errors about success typing and the contract. If I had to guess based on previous dialyzer battles, this is because the Plug.Conn typespec has keys which need a value (like pid), and the argument we are passing is primarily empty

I suppressed dialyzer for that file to make progress

@dlobo good call, should probably do something similar to Plug.Adapters.Test.Conn: https://github.com/elixir-plug/plug/blob/f24af6a5b00dba4d32d44c6f2fb4423f6a5d8e3b/lib/plug/adapters/test/conn.ex#L34-L50

In my case, the Pow calls just needed the config object (basically I wanted to delete the existing tokens in case the user updated the roles, so deleting the token in persistent and credentials cache)

so I just sent in the config as: [otp_app: :glific] and did not need the conn object (and hence did not need to get in via absinthe either)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blatyo picture blatyo  路  4Comments

mreishus picture mreishus  路  5Comments

dweremeichik picture dweremeichik  路  7Comments

Kaquadu picture Kaquadu  路  4Comments

danschultzer picture danschultzer  路  5Comments