Tutorial
Multi-tenancy and Phoenix - Part 2
This post was updated 01 May - 2020
authenticationmultitenancyphoenixpow
In the [previous tutorial](https://fullstackphoenix.com/tutorials/multi-tenancy-and-authentication-with-pow), I wrote how to set up multi-tenancy with Phoenix and Pow. In this tutorial, I will show how I scope resources the current users account. And also, how to load the current account in a plug and make the tests pass.
### STEP 1 - Generate a new resource
The first step is to generate a new resource. Since it requires a sign in, I will call it Secrets. And since I want this to be scoped to a specific account, i need to make a relation to the `accounts` table. I will do so by adding a reference like `account_id:references:accounts`
mix tailwind.gen.html Secrets Secret secrets account_id:references:accounts key value
**NOTE** I use my own custom Tailwind html generator that I introduced in [this tutorial](https://fullstackphoenix.com/tutorials/add-tailwind-html-generators-in-phoenix)
THe first file to modify is the migration file. Change the line:
# priv/repo/migrations/20200316074930_create_secrets.exs
add :account_id, references(:accounts, on_delete: :nothing)
to this:
add :account_id, references(:accounts, on_delete: :delete_all), null: false
What did I actually change, and why?
1. I want to make sure that every row I store in the database has an account.
2. Also make sure that the account that I point to actually exists in the accounts table
3. Lastly, if I delete an account row, I want to cascade and delete all secrets.
Next, I need to add `resources "/secrets", SecretController` to the routes before I run the migrations.
# lib/tutorial_web/router.ex
defmodule TutorialWeb.Router do
...
scope "/", TutorialWeb do
pipe_through [:protected, :browser]
get "/private", PageController, :private
resources "/secrets", SecretController
end
...
end
With that in place, I can run the migration.
mix ecto.migrate
### STEP 2 - Update context and tests
If I run the tests in `test/tutorial/secrets_test.exs` they would almost all fail.
To get that passing, I need to complete these 3 steps
1. Update `Secret` and setup the correct relation in the schema to `Account`
2. Go through `Secrets` and take `account` into account(!) for some of the function calls
3. Update tests so I create an account and pass them in
defmodule Tutorial.Secrets.Secret do
...
schema "secrets" do
field :key, :string
field :value, :string
belongs_to :account, Tutorial.Accounts.Account
timestamps()
end
...
end
I also need to setup the relation in the `Account` module.
# lib/tutorial/accounts/account.ex
defmodule Tutorial.Accounts.Account do
...
schema "accounts" do
field :name, :string
has_many :secrets, Tutorial.Secrets.Secret
timestamps()
end
...
end
In `Secrets` context module I basically need to change in three places. Firsts is listing secrets `list_secrets/0`
# lib/tutorial/secrets.ex
def list_secrets do
Repo.all(Secret)
end
Change that to `list_secrets/1` and pass in the account
# lib/tutorial/secrets.ex
def list_secrets(account) do
from(s in Secret, where: s.account_id == ^account.id, order_by: [asc: :id])
|> Repo.all()
end
Furter down is the `get_secret!/1`
# lib/tutorial/secrets.ex
def get_secret!(id), do: Repo.get!(Secret, id)
Change that to `get_secret!/2` and again, pass in account
# lib/tutorial/secrets.ex
def get_secret!(account, id) do
Repo.get_by!(Secret, account_id: account.id, id: id)
end
Finally, last function to change here is creating a secret. I need to change `create_secret/1`.
# lib/tutorial/secrets.ex
def create_secret(attrs \\ %{}) do
%Secret{}
|> Secret.changeset(attrs)
|> Repo.insert()
end
Again, I need to pass in the account and build the relationship to secrets in `create_secret/2`
# lib/tutorial/secrets.ex
def create_secret(account, attrs \\ %{}) do
Ecto.build_assoc(account, :secrets)
|> Secret.changeset(attrs)
|> Repo.insert()
end
Last part here is to update the tests. I need to add an account fixture so I can pass in an account to all the tests. I also setup all the data creation to a setup-function:
# test/tutorial/secrets_test.exs
def account_fixture() do
{:ok, account} = Tutorial.Accounts.create_account(%{name: "Acme Corp"})
account
end
def secret_fixture(account, attrs \\ %{}) do
{:ok, secret} =
Secrets.create_secret(
account,
Enum.into(attrs, @valid_attrs)
)
secret
end
setup do
account = account_fixture()
secret = secret_fixture(account)
{:ok, %{account: account, secret: secret}}
end
Next I need to go trough the test and change from
# test/tutorial/secrets_test.exs
test "list_secrets/0 returns all secrets" do
secret = secret_fixture()
assert Secrets.list_secrets() == [secret]
end
# test/tutorial/secrets_test.exs
test "list_secrets/0 returns all secrets" do
secret = secret_fixture()
assert Secrets.list_secrets() == [secret]
end
# test/tutorial/secrets_test.exs
test "list_secrets/0 returns all secrets", %{account: account, secret: secret} do
assert Secrets.list_secrets(account) == [secret]
end
When I have updated all the tests, I can run in the console
mix test test/tutorial/secrets_test.exs
And they should all pass

### STEP 3 - Create a plug for loading the account
When I have my context specs passing, I can now look at the controller tests. I already now know that I need the account in all the controller actions. I usually solve this by creating a plug that loads the account and assign it to the `conn`.
I will create the plug `SetCurrentAccount` and it works by if there is a current_user, that comes from the Pow Plug, then also get the current account and assign that.
# lib/tutorial_web/plugs/set_current_account.ex
defmodule TutorialWeb.Plugs.SetCurrentAccount do
import Plug.Conn, only: [assign: 3]
alias Tutorial.Repo
alias Tutorial.Users.User
def init(options), do: options
def call(conn, _opts) do
case conn.assigns[:current_user] do
%User{} = user ->
%User{account: account} = Repo.preload(user, :account)
assign(conn, :current_account, account)
_ ->
assign(conn, :current_account, nil)
end
end
end
In the router, I will then add it *after* `Pow.Plug.RequireAuthenticated`
# lib/tutorial_web/router.ex
pipeline :protected do
plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
plug TutorialWeb.Plugs.SetCurrentAccount
end
### STEP 4 - Updating Controller and tests
Last part here is to go through all the controller action functions and take the account from the `conn` and pass is it in when loading or creating the resource. So change from
# lib/tutorial_web/controllers/secret_controller.ex
defmodule TutorialWeb.SecretController do
use TutorialWeb, :controller
...
def index(conn, _params) do
secrets = Secrets.list_secrets()
render(conn, "index.html", secrets: secrets)
end
...
end
to:
# lib/tutorial_web/controllers/secret_controller.ex
defmodule TutorialWeb.SecretController do
use TutorialWeb, :controller
...
def index(conn, _params) do
current_account = conn.assigns.current_account
secrets = Secrets.list_secrets(current_account)
render(conn, "index.html", secrets: secrets)
end
...
end
When it comes to the tests, I need to do two main things. First accomodate for creating an account and user fixtures. But I also need to remember that all routes are protected so I need to setup a login feature.
I deleted the setups that where generated in the tests and add:
# test/tutorial_web/controllers/secret_controller_test.exs
defmodule TutorialWeb.SecretControllerTest do
use TutorialWeb.ConnCase
alias Tutorial.Secrets
alias Tutorial.Repo
alias Tutorial.Users.User
...
defp login(%{conn: conn}) do
user = create_user()
conn = Pow.Plug.assign_current_user(conn, user, otp_app: :mail_flow_admin)
{:ok, conn: conn}
end
defp create_user do
user_attrs = %{account_name: "Acme Corp", email: "john.doe@example.com", password: "SuperSecret123", password_confirmation: "SuperSecret123"}
{:ok, user} = User.changeset(%User{}, user_attrs)
|> Repo.insert()
user
|> Tutorial.Repo.preload(:account)
end
defp create_secret(%{conn: conn}) do
{:ok, secret} =
case conn.assigns do
%{current_user: %{account: account}} -> Secrets.create_secret(account, @create_attrs)
_ ->
another_account = create_user().account
Secrets.create_secret(another_account, @create_attrs)
end
{:ok, secret: secret}
end
...
end
Then I change so I have two versions for the tests. One when a user is signed in and the route is accessible and one without logging in and the user gets redirected.
# test/tutorial_web/controllers/secret_controller_test.exs
defmodule TutorialWeb.SecretControllerTest do
...
describe "index as not logged in" do
test "redirects to login", %{conn: conn} do
conn = get(conn, Routes.secret_path(conn, :index))
assert redirected_to(conn) =~ Routes.pow_session_path(conn, :new)
end
end
describe "index" do
setup [:login]
test "lists all secrets", %{conn: conn} do
conn = get(conn, Routes.secret_path(conn, :index))
assert html_response(conn, 200) =~ "Secrets"
end
end
...
# rest of the tests
end
So, after I have gone through the tests and created two versions of them, they should all pass.