Pow: Endpoint plugs for multiple user schemas

Created on 15 Jul 2019  路  4Comments  路  Source: danschultzer/pow

Hi,
I'm following guide for multiple users in same scope:
https://github.com/danschultzer/pow/blob/e725d04b2a9e91565a1c7c1075411e0ae2cec792/guides/MULTIPLE_USERS_IN_SAME_SCOPE.md

My problem is that when I register a new user - no matter via admin scope or via user scope - my app uses only the last Session plug in endpoint.ex file. It seems, that somehow the namespace configuration is not passed.

Here is my router.ex:

...
  scope "/", private: %{pow_config_namespace: :user} do
    pipe_through [:browser, :locked]
    pow_routes()
    pow_extension_routes()
    pow_assent_routes()
  end

  scope "/admin", private: %{pow_namespace: :admin}, as: :admin do
    pipe_through :browser
    pow_routes()
  end

  scope "/", BaseAppWeb, private: %{pow_namespace: :user} do
    pipe_through [:browser, :locked]
    get "/", PageController, :index
    get "/static", PageController, :show_static
  end

  scope "/panel", BaseAppWeb do
    pipe_through :browser
    get "/", AdminController, :index
    get "/users", AdminUserController, :index
    get "/block_user", AdminUserController, :block_user
    scope "/static_pages" do
      get "/", StaticPagesController, :index
      get "/new_page", StaticPagesController, :new
      post "/create_page", StaticPagesController, :create
      get "/edit_page", StaticPagesController, :edit
      post "/update_page", StaticPagesController, :update
      get "/delete_page", StaticPagesController, :delete
    end
  end
...

and endpoint.ex:

defmodule BaseAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :base_app

  socket "/socket", BaseAppWeb.UserSocket,
    websocket: true,
    longpoll: false

  plug Plug.Static,
    at: "/",
    from: :base_app,
    gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt)

  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head

  plug Plug.Session,
    store: :cookie,
    key: "_base_app_key",
    signing_salt: "salt"

  plug Pow.Plug.Session,
    repo: BaseApp.Repo,
    user: BaseApp.Auth.Admin,
    namespace: :admin,
    current_user_assigns_key: :current_admin

  plug Pow.Plug.Session,
    repo: BaseApp.Repo,
    user: BaseApp.Auth.User,
    namespace: :user,
    current_user_assigns_key: :current_user

  plug BaseAppWeb.Router
end

In case of this code, no matter if I register from /admin/registration/new or /registration/new, my app always creates BaseApp.Auth.User - because plug with user: BaseApp.Auth.User comes after a plug with user: BaseApp.Auth.Admin in endpoint.ex. In case when I switch their order - it creates only admin users.

Most helpful comment

That guide is from a deleted PR branch: #24

I rejected it due to https://github.com/danschultzer/pow/issues/19#issuecomment-439668816. The current recommended way of dealing with this is to set up an umbrella app with a separate Phoenix app for each user context (all keys will automatically have the otp app namespace, and you'll have clear separation).

You can still deal with it in the same Phoenix app, but in that case you will have to ensure that all admin routes has the admin configuration as the primary config.

It's assume that you would like to have access to the admin user anywhere, so first we need the two plug calls in the endpoint:

defmodule MyAppWeb.Endpoint do
  # ...

  plug Pow.Plug.Session,
    repo: MyApp.Repo,
    user: MyApp.Users.Admin,
    current_user_assigns_key: :current_admin,
    session_key: "admin_auth"

  plug Pow.Plug.Session,
    repo: MyApp.Repo,
    user: MyApp.Users.User

  plug BaseAppWeb.Router
end

Then we'll need to ensure that only the admin config is used for admin Pow paths:

  pipeline :pow_admin do
    plug :set_pow_config,
      repo: MyApp.Repo,
      user: MyApp.Users.Admin,
      current_user_assigns_key: :current_admin,
      session_key: "admin_auth",
      routes_backend: MyAppWeb.Pow.AdminRoutes,
      plug: Pow.Plug.Session
  end

  defp set_pow_config(conn, config), do: Pow.Plug.put_config(conn, config)

  scope "/admin", as: :admin do
    pipe_through [:browser, :pow_admin]
    pow_routes()
  end

And we'll have to ensure all paths generated within that pipeline are the admin paths. Something like this should work (it will prepend the controller module name with Admin so that admin_pow_* route helpers are called instead of the pow_* routes):

defmodule MyAppWeb.Pow.AdminRoutes do
  use Pow.Phoenix.Routes

  def path_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.path_for(conn, plug, vars, query_params)
  end

  def url_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.url_for(conn, plug, vars, query_params)
  end
end

You can put the admin Pow config in your config.exs and load them in both places on compile time with Application.get_env/2.

Be advised that I haven't tested the above, but this is more or less what you need to do to get it working. There are some caveats, so I recommend to just use an umbrella setup if possible.

All 4 comments

That guide is from a deleted PR branch: #24

I rejected it due to https://github.com/danschultzer/pow/issues/19#issuecomment-439668816. The current recommended way of dealing with this is to set up an umbrella app with a separate Phoenix app for each user context (all keys will automatically have the otp app namespace, and you'll have clear separation).

You can still deal with it in the same Phoenix app, but in that case you will have to ensure that all admin routes has the admin configuration as the primary config.

It's assume that you would like to have access to the admin user anywhere, so first we need the two plug calls in the endpoint:

defmodule MyAppWeb.Endpoint do
  # ...

  plug Pow.Plug.Session,
    repo: MyApp.Repo,
    user: MyApp.Users.Admin,
    current_user_assigns_key: :current_admin,
    session_key: "admin_auth"

  plug Pow.Plug.Session,
    repo: MyApp.Repo,
    user: MyApp.Users.User

  plug BaseAppWeb.Router
end

Then we'll need to ensure that only the admin config is used for admin Pow paths:

  pipeline :pow_admin do
    plug :set_pow_config,
      repo: MyApp.Repo,
      user: MyApp.Users.Admin,
      current_user_assigns_key: :current_admin,
      session_key: "admin_auth",
      routes_backend: MyAppWeb.Pow.AdminRoutes,
      plug: Pow.Plug.Session
  end

  defp set_pow_config(conn, config), do: Pow.Plug.put_config(conn, config)

  scope "/admin", as: :admin do
    pipe_through [:browser, :pow_admin]
    pow_routes()
  end

And we'll have to ensure all paths generated within that pipeline are the admin paths. Something like this should work (it will prepend the controller module name with Admin so that admin_pow_* route helpers are called instead of the pow_* routes):

defmodule MyAppWeb.Pow.AdminRoutes do
  use Pow.Phoenix.Routes

  def path_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.path_for(conn, plug, vars, query_params)
  end

  def url_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.url_for(conn, plug, vars, query_params)
  end
end

You can put the admin Pow config in your config.exs and load them in both places on compile time with Application.get_env/2.

Be advised that I haven't tested the above, but this is more or less what you need to do to get it working. There are some caveats, so I recommend to just use an umbrella setup if possible.

Thank you for a quick answer!
I just wanted to avoid umbrella app, so I followed your instructions. This solution works really good when it comes to adding users to the right table, however in the AdminRoutes module it was necessary to make some slight changes, here is my code:

defmodule MyAppWeb.Pow.AdminRoutes do
    use Pow.Phoenix.Routes

    def path_for(conn, plug, verb, vars \\ [], query_params \\ []) do
      plug = Module.concat(["admin_pow", plug])
      Pow.Phoenix.Routes.path_for(conn, plug, verb, vars, query_params)
    end

    def url_for(conn, plug, verb, vars \\ [], query_params \\ []) do
      plug = Module.concat(["admin_pow", plug])
      Pow.Phoenix.Routes.url_for(conn, plug, verb, vars, query_params)
    end
end

And also although the user is added to the right table - when it comes to admin users I've got a small problem. After creating an admin user I'm becoming this error: Pow plug was not found in config. Please use a Pow plug that puts the `:plug` in the Pow configuration. Probably I'm missing something really goofy, but I can't find damn thing.

Good catch, I've updated the example!

I forgot that the plug is necessary for Pow to update session, it's something that's automatically added to the config when running the plug:

  pipeline :pow_admin do
    plug :set_pow_config,
      repo: MyApp.Repo,
      user: MyApp.Users.Admin,
      current_user_assigns_key: :current_admin,
      session_key: "admin_auth",
      routes_backend: MyAppWeb.Pow.AdminRoutes,
      plug: Pow.Plug.Session
  end

This will call the Pow.Plug.Session to renew session 馃槃

At some point I'll write a guide on all this, so please let me know if things are working as expected or if there are any caveats.

Everything works fine now 馃槃
Thank you for help!

Was this page helpful?
0 / 5 - 0 ratings