New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Pow.PasswordReset.Plug.create_reset_token without a conn #61

Closed
axelclark opened this Issue Jan 4, 2019 · 4 comments

Comments

Projects
None yet
2 participants
@axelclark
Copy link
Contributor

axelclark commented Jan 4, 2019

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.

@axelclark

This comment has been minimized.

Copy link
Contributor Author

axelclark commented Jan 5, 2019

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
@danschultzer

This comment has been minimized.

Copy link
Owner

danschultzer commented Jan 6, 2019

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)
@danschultzer

This comment has been minimized.

Copy link
Owner

danschultzer commented Jan 6, 2019

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 😄

@danschultzer

This comment has been minimized.

Copy link
Owner

danschultzer commented Jan 9, 2019

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment