pow
pow copied to clipboard
Multitenancy with Pow guide confusing
I'm trying to use the "Multitenancy with Pow" guide to set up authentication in an app that uses Triplex. The app creates a db prefix for each "Customer", so they can create their own blogs (as example content). Each Customers content should be separated after signing in.
After following the Triplex part of the guide I get:
Pow.Config.ConfigError at GET /session/new
No `:user` configuration option found.
Does it need repo_opts in the config? I would want to use something like repo_opts: [prefix: :current_tenant]
... Should I use Pow's user migration as a tenant_migration?
Sounds like the Pow config is not set right? I would check the config
in MyAppWeb.Pow.TriplexSessionPlug.call/2
has the right :otp_app
setting.
Are the users under the tenants as well? Then the guide is how it should be set up. The :repo_opts
will be set dynamically in the MyAppWeb.Pow.TriplexSessionPlug
plug.
I copied the example SessionPlug from the guide.
defmodule TenantzWeb.PowTriplexSessionPlug do
def init(config), do: config
def call(conn, config) do
tenant = conn.assigns[:current_tenant] || conn.assigns[:raw_current_tenant]
prefix = Triplex.to_prefix(tenant)
config = Keyword.put(config, :repo_opts, prefix: prefix)
Pow.Plug.Session.call(conn, config)
end
end
Are the users under the tenants as well?
That's one of the scenario's I'm comparing. In that case should the user migration happen for each tenant?
I copied the example SessionPlug from the guide.
Have you set it with the proper :otp_app
config in the endpoint?
plug TenantzWeb.PowTriplexSessionPlug, otp_app: :my_app
That's one of the scenario's I'm comparing. In that case should the user migration happen for each tenant?
Yeah.
Ah found the error, I made a typo in the :app_name in the endpoint config... Thanks! 👍
Sorry to have to reopen. How would I get Pow.Plug.create_user to use the current_tenant?
Once the TenantzWeb .PowTriplexSessionPlug
has been triggered you don't need to do anything as the :repo_opts
has been set for the config.
Somehow it doesn't.
Postgrex.Error at POST /firstcustomer/signup
ERROR 42P01 (undefined_table) relation "users" does not exist
As you can see I use the tenant_id in the url, I created a custom registration controller etc. What is the setup you use in the guide? I tried the default Pow registration path first, should I add some kind of session param to the form? How does Pow know which tentant it should use?
Check here: https://github.com/joepstender/testpow
You should move the plug Triplex.ParamPlug
to before plug TestpowWeb.PowTriplexSessionPlug
. I've added a section to the guide: https://github.com/danschultzer/pow/pull/495
Unfortunately that doesn't seem to work. Same error message. I propose I write a step-by-step guide that could be checked and perhaps when we get it working used for https://powauth.com ?
Postgrex.Error at POST /firstcustomer/signup
ERROR 42P01 (undefined_table) relation "users" does not exist
query: INSERT INTO "users" ("email","password_hash","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id"
Sorry for the delay, got occupied by a lot of work. Let me test this locally.
Works for me, I've added the demo here: https://github.com/pow-auth/pow_demo/compare/master..multitenancy-triplex
Also, looking at the test repo, seems like there's no triplex plug?
Thanks for reopening. I added the triplex plug here. In your test you do:
defp set_triplex_tenant(conn, tenant) do
conn = %{conn | params: %{"subdomain" => tenant}}
opts = Triplex.ParamPlug.init(param: :subdomain)
Triplex.ParamPlug.call(conn, opts)
end
Perhaps I misunderstand how Triplex params work? I use the slug for the tenant. In tenant.ex:
@derive {Phoenix.Param, key: :slug}
Then I get the tenant from the db using the slug:
def get_tenant!(slug) do
Tenant
|> Repo.get_by!(%{slug: slug})
end
In the controller:
tenant = Testpow.Customers.get_tenant!(conn.params["tenant_id"])
def create(conn, %{"user" => user_params}, tenant) do
conn
|> Pow.Plug.create_user(user_params)
|> case do
{:ok, _user, conn} ->
conn
|> put_flash(:info, "Welcome!")
|> redirect(to: Routes.tenant_path(conn, :show, tenant))
{:error, changeset, conn} ->
render(conn, "new.html", changeset: changeset, tenant: tenant)
end
end
Yeah, it's the Triplex plug that's missing: https://hexdocs.pm/triplex/1.3.0/readme.html#fetching-the-tenant-with-plug
It's easiest if you pull the tenant before calling the TestpowWeb.PowTriplexSessionPlug
plug, so in your case I would set it up like this rather than pulling it in the controller:
plug Triplex.ParamPlug,
param: :tenant_id,
tenant_handler: &Testpow.Customers.get_tenant!/1
plug TestpowWeb.PowTriplexSessionPlug, otp_app: :testpow
Hello,
I have read though this thread and am also having difficulties with being able to get the tenant / subdomain to use for the session.
It really seems that I am really close to having this up and running, but setting the tenant to the subdomain doesn't seem to be working.
I one of my index files, invoke pry, and in the conn:
raw_current_tenant: nil repo_opts: [prefix: nil]
I am running on localhost, and setup a domain one.whatevertesting.com:4000
However, if I change the triplex_session_plug for the config like this:
config = Keyword.put(config, :repo_opts, prefix: "one")
In this case the repo_opts: [prefix: "one"] - as it should be
but raw_current_tenant is still nil
Even if that is set, it does not seem to propagate through other files. Do you still need to update the functions in the Context ie:
def list_clients do
Repo.all(Client, prefix: Triplex.to_prefix("one"))
end
That is the only way that I can seem to get the listing from the tenant. If I run without the prefix code, is shows the public repository. (I converted a non-tenant app before adding the tenancy)
Arni
ps. Thanks for this I had POW up and running easily, and seem to be close to having this as well. Looking forward to getting over the (hopefully) last hurdle
@arnimikelsons you might not be setting the Triplex plug before the MyAppWeb.PowTriplexSessionPlug
plug call in your endpoint:
plug Triplex.SubdomainPlug,
endpoint: MyApp.Endpoint
# ...
plug MyAppWeb.PowTriplexSessionPlug, otp_app: :my_app
Thanks for the quick reply.
When I put that in, I get an error:
ERROR 42P01 (undefined_table) relation "one.whatevertesting.com.users" does not exist
(the tenant is "one")
I tried changing the instance name from "one" to "one.whatevertesting.com", but that didn't work as well. I guess would need to get in touch with the Triplex people to do that??? I have been playing with a tutorial on subdomains, that does manage to pull out just the first part of the URL at: https://blog.gazler.com/blog/2015/07/18/subdomains-with-phoenix/
I cannot get Triplex working correctly when putting the (Param)Plug in endpoint.ex, when I put it in the router (like Triplex documentation suggests) the :tenant, and :raw_current_tenant become available from the param.
The Triplex.ParamPlug
plug Triplex.ParamPlug,
param: :id,
tenant_handler: &MyAppWeb.TenantHelper.tenant_handler/1
And the TenantHelper module, using Triplex.exists? to avoid errors on non-tenant pages. If I use that :current_tenant won't be loaded.
defmodule MyAppWeb.TenantHelper do
alias MyApp.Customers
def tenant_handler(id) do
if Triplex.exists?(id) do
customer = Customers.get_customer!(id)
Triplex.to_prefix(customer)
end
end
end
Next step is to test with Pow. I'll report back :-)
@arnimikelsons yeah, it's a confusing error.
The error suggests that you haven't created the tenant with Triplex.create("one.whatevertesting.com")
or haven't moved the user migration from priv/YOUR_REPO/migrations
to priv/YOUR_REPO/tenant_migrations
.
However it really should just be pulling out one
instead of the whole domain. Looking at the source code, it seems you need to set the whatevertesting.com
domain in the url
endpoint setting for it to pull the subdomain. Should look something like this:
config :my_app, MyAppWeb.Endpoint,
url: [host: "whatevertesting.com"],
# ...
@joepstender I realize that I'm actually not testing the controller in the demo branch I linked above. Let me add a proper controller test to see if it works. I suspect that query params might not have been fetched.
Edit: Added controller tests, and it works: https://github.com/pow-auth/pow_demo/commit/297d8558b0c31105b7c357bf64c109ee07eb36a4
I figured out that error by adding the host. Actually, would be good to the docs when you are editing the config file, that you need to do that. ie. put in the host name if you are using subdomains. It is obvious now that I see it, but it took a while. Still not quite connecting everything....
I have raw_current_user set as a private variable to "one" (picking it up from the subdomain)
I presume it should set the variable tenant in TriplexSessionPlug, so that it can be used elsewhere.
But if I try to put in: def list_clients do Repo.all(Client, prefix: Triplex.to_prefix(tenant)) end I get lib/client_app/client_base.ex:21: undefined function tenant/0. Do i need a function in the context to grab that variable? I would have thought the plug makes it available
def list_clients do Repo.all(Client, prefix: Triplex.to_prefix("one") end
It does list the client in that tenant. If I remove the prefix, it lists the public repository (I had it there before migrating it)
Sorry if these questions are basic, just learning the Phoenix / Elixir.
Actually, would be good to the docs when you are editing the config file, that you need to do that. ie. put in the host name if you are using subdomains.
I agree. I think this is something the Triplex docs should have rather than Pow.
I presume it should set the variable tenant in TriplexSessionPlug, so that it can be used elsewhere.
It should be set by one of the triplex plugs: https://hexdocs.pm/triplex/1.3.0/readme.html#fetching-the-tenant-with-plug
Then it would be available with :current_tenant
assigns. The Pow triplex demo should show how: https://github.com/pow-auth/pow_demo/compare/master..multitenancy-triplex
It might be good to add some logic to ensure that the tenant has been loaded though. I'll update the multitenancy guide to make that clearer.
If you don't want to use the Triplex plugs then I guess you could change the TriplexSessionPlug
to set the tenant.
The problem in your code is that you don't pass the tenant to the method, should look like this:
def list_clients(tenant) do
Repo.all(Client, prefix: Triplex.to_prefix(tenant))
end
Thanks. What do I put in the controller to pass the subdomain over?
I have:
def index(conn, _params) do clients = ClientBase.list_clients("one") render(conn, "index.html", clients: clients) end
But, struggling how to turn the "one" into a variable
Actually, I got it:
def index(conn, _params) do clients = ClientBase.list_clients(conn.private[:subdomain]) render(conn, "index.html", clients: clients) end
Looks good, but you can also assign the prefix in your custom Pow Triplex plug:
defmodule MyAppWeb.Pow.TriplexSessionPlug do
def init(config), do: config
def call(conn, config) do
tenant = conn.assigns[:current_tenant] || conn.assigns[:raw_current_tenant]
prefix = Triplex.to_prefix(tenant)
config = Keyword.put(config, :repo_opts, [prefix: prefix])
conn
|> Plug.Conn.assign(:repo_prefix, prefix)
|> Pow.Plug.Session.call(conn, config)
end
end
And then you can pass it like this:
def index(conn, _params) do
clients = ClientBase.list_clients(conn.assigns[:repo_prefix])
render(conn, "index.html", clients: clients)
end
And have the list_clients
method look like this:
def list_clients(prefix) do
Repo.all(Client, prefix: prefix)
end
Seems that this is not taking the triplex prefix. Where would you add that?
Another note, which is kind of weird. If you have <%= require IEx; IEx.pry %> in the template (index.html.eex, in this case), then you get an "protocol Phoenix.HTML.Safe not implemented" error. Remove the line and the error goes away.
Running into another problem with Registration. On submission, I am getting
ERROR 23502 (not_null_violation) null value in column "account_id" violates not-null constraint
This was working before using multi-tenancy, so I suspect that the relation is not on the tenant. Specifically, would need to include it into the tenant when running create_new_account_for_user, and not sure how to do that.
I have got the ClientAppWeb.Pow.TriplexSessionPlug working, so that I am getting the following values:
prefix: "org_two", tenant: "two"
It is from this tutorial: https://fullstackphoenix.com/tutorials/multi-tenancy-and-authentication-with-pow
That tutorial handles multitenancy in a very different way, keeping all tenant data within the same DB tables. If you go for that approach, then you should scrap Triplex.
Sorry, the multi-tenancy is in the next tutorial - https://fullstackphoenix.com/tutorials/multi-tenancy-and-phoenix-part-2. I think it is good, and probably should just have all user fields in one table. Will take another shot at it.
I have redone it, with your instructions. Only thing, I could not get the Triplix plug to work. Maybe because it is localhost domains.. This is what I use:
defmodule TestpowWeb.Plug.Subdomain do
import Plug.Conn
@doc false
def init(default), do: default
@doc false
def call(conn, _router) do
case get_subdomain(conn.host) do
subdomain when byte_size(subdomain) > 0 ->
conn
|> put_private(:subdomain, subdomain)
|> put_private(:current_raw_tenant, subdomain)
_ ->
conn
end
end
defp get_subdomain(host) do
root_host = TestpowWeb.Endpoint.config(:url)[:host]
String.replace(host, ~r/.?#{root_host}/, "")
end
end
And change the the tenant line in triplex_session_plug.ex to:
tenant = conn.private[:subdomain]
Which seems to work great