Skip to content

Commit 9a5ea4d

Browse files
authored
improvement: Add subdomain live_view hook (#339)
1 parent f3e73d7 commit 9a5ea4d

File tree

4 files changed

+210
-32
lines changed

4 files changed

+210
-32
lines changed

lib/ash_phoenix/helpers.ex

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule AshPhoenix.Helpers do
2+
def get_subdomain(%Plug.Conn{host: host}, endpoint) when is_atom(endpoint) do
3+
get_subdomain(host, endpoint)
4+
end
5+
6+
def get_subdomain(%Phoenix.Socket{endpoint: endpoint}, url) when is_binary(url) do
7+
url
8+
|> URI.parse()
9+
|> Map.get(:host)
10+
|> get_subdomain(endpoint)
11+
end
12+
13+
def get_subdomain(host, endpoint) when is_atom(endpoint) do
14+
root_host = endpoint.config(:url)[:host]
15+
16+
if host in [root_host, "localhost", "127.0.0.1", "0.0.0.0"] do
17+
nil
18+
else
19+
host
20+
|> String.trim_leading("www.")
21+
|> String.replace(~r/.?#{root_host}/, "")
22+
|> case do
23+
"" ->
24+
nil
25+
26+
subdomain ->
27+
subdomain
28+
end
29+
end
30+
end
31+
32+
def get_subdomain(_, _), do: nil
33+
end
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
defmodule AshPhoenix.LiveView.SubdomainHook do
2+
@hook_options [
3+
assign: [
4+
type: :atom,
5+
doc: "The key to use when assigning the current tenant",
6+
default: :current_tenant
7+
],
8+
handle_subdomain: [
9+
type: :mfa,
10+
doc:
11+
"An mfa to call with the socket and a subdomain value. Can be used to do something like fetch the current user given the tenant.
12+
Must return either `{:cont, socket}`, `{:cont, socket, opts} or `{:halt, socket}`."
13+
]
14+
]
15+
16+
@moduledoc """
17+
This is a basic hook that loads the current tenant assign from a given
18+
value set on subdomain.
19+
20+
Options:
21+
22+
#{Spark.Options.docs(@hook_options)}
23+
24+
To use the hook, you can do one of the following:
25+
26+
```elixir
27+
live_session :foo, on_mount: [
28+
AshPhoenix.LiveView.SubdomainHook,
29+
]
30+
```
31+
This will assign the tenant's subdomain value to `:current_tenant` key by default.
32+
33+
If you want to specify the assign key
34+
35+
```elixir
36+
live_session :foo, on_mount: [
37+
{AshPhoenix.LiveView.SubdomainHook, [assign: :different_assign_key}]
38+
]
39+
```
40+
41+
You can also provide `handle_subdomain` module, function, arguments tuple
42+
that will be run after the tenant is assigned.
43+
44+
```elixir
45+
live_session :foo, on_mount: [
46+
{AshPhoenix.LiveView.SubdomainHook, [handle_subdomain: {FooApp.SubdomainHandler, :handle_subdomain, [:bar]}]
47+
]
48+
```
49+
50+
This can be any module, function, and list of arguments as it uses Elixir's [apply/3](https://hexdocs.pm/elixir/1.18.3/Kernel.html#apply/3).
51+
52+
The socket and tenant will be the first two arguments.
53+
54+
The function return must match Phoenix LiveView's [on_mount/1](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1)
55+
56+
```elixir
57+
defmodule FooApp.SubdomainHandler do
58+
def handle_subdomain(socket, tenant, :bar) do
59+
# your logic here
60+
{:cont, socket}
61+
end
62+
end
63+
```
64+
"""
65+
66+
import Phoenix.LiveView
67+
import Phoenix.Component, only: [assign: 3]
68+
69+
def on_mount(opts, _params, _session, socket) when is_list(opts) do
70+
opts = Spark.Options.validate!(opts, @hook_options)
71+
72+
socket
73+
|> assign_tenant(opts)
74+
|> call_handle_subdomain(opts)
75+
end
76+
77+
def on_mount(_action, params, session, socket) do
78+
on_mount([], params, session, socket)
79+
end
80+
81+
defp assign_tenant(socket, opts) do
82+
attach_hook(socket, :set_tenant, :handle_params, fn
83+
_params, url, socket ->
84+
subdomain = AshPhoenix.Helpers.get_subdomain(socket, url)
85+
{:cont, assign(socket, opts[:assign], subdomain)}
86+
end)
87+
end
88+
89+
defp call_handle_subdomain(socket, opts) do
90+
case opts[:handle_subdomain] do
91+
{m, f, a} ->
92+
apply(m, f, [socket, socket.assigns[opts[:assign]] | a])
93+
94+
_ ->
95+
{:cont, socket}
96+
end
97+
end
98+
end

lib/ash_phoenix/plug/subdomain_plug.ex

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,13 @@ defmodule AshPhoenix.SubdomainPlug do
5555
{:noreply, socket}
5656
end
5757
"""
58-
alias Plug.Conn
59-
6058
@doc false
6159
def init(opts), do: Spark.Options.validate!(opts, @plug_options)
6260

6361
@doc false
6462
def call(conn, opts) do
65-
subdomain = get_subdomain(conn, opts)
63+
subdomain =
64+
AshPhoenix.Helpers.get_subdomain(conn, opts[:endpoint])
6665

6766
conn
6867
|> Plug.Conn.assign(opts[:assign], subdomain)
@@ -71,10 +70,7 @@ defmodule AshPhoenix.SubdomainPlug do
7170

7271
if Code.ensure_loaded?(Phoenix.LiveView) do
7372
def live_tenant(socket, url) do
74-
url
75-
|> URI.parse()
76-
|> Map.get(:host)
77-
|> do_get_subdomain(socket.endpoint.config(:url)[:host])
73+
AshPhoenix.Helpers.get_subdomain(socket, url)
7874
end
7975
end
8076

@@ -87,29 +83,4 @@ defmodule AshPhoenix.SubdomainPlug do
8783
conn
8884
end
8985
end
90-
91-
defp get_subdomain(
92-
%Conn{host: host},
93-
opts
94-
) do
95-
root_host = opts[:endpoint].config(:url)[:host]
96-
do_get_subdomain(host, root_host)
97-
end
98-
99-
defp do_get_subdomain(host, root_host) do
100-
if host in [root_host, "localhost", "127.0.0.1", "0.0.0.0"] do
101-
nil
102-
else
103-
host
104-
|> String.trim_leading("www.")
105-
|> String.replace(~r/.?#{root_host}/, "")
106-
|> case do
107-
"" ->
108-
nil
109-
110-
subdomain ->
111-
subdomain
112-
end
113-
end
114-
end
11586
end

test/helpers_test.exs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
defmodule AshPhoenix.HelpersTest do
2+
use ExUnit.Case
3+
4+
defmodule TestEndpoint do
5+
@config [
6+
url: [host: "example.com"]
7+
]
8+
9+
def config(key, default \\ nil) do
10+
Access.get(@config, key, default)
11+
end
12+
end
13+
14+
describe "get_subdomain/2" do
15+
test "when non-local host contains subdomain and endpoint returns subdomain" do
16+
tenant = "tenant"
17+
host = "#{tenant}.example.com"
18+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
19+
20+
assert subdomain == tenant
21+
end
22+
23+
test "when url host = endpoint host returns nil" do
24+
host = "example.com"
25+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
26+
27+
assert is_nil(subdomain)
28+
end
29+
30+
test "with url host is localhost returns nil" do
31+
host = "localhost"
32+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
33+
34+
assert is_nil(subdomain)
35+
end
36+
37+
test "when url host is 127.0.0.1 returns nil" do
38+
host = "127.0.0.1"
39+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
40+
41+
assert is_nil(subdomain)
42+
end
43+
44+
test "when url host is 0.0.0.0 returns nil" do
45+
host = "0.0.0.0"
46+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
47+
48+
assert is_nil(subdomain)
49+
end
50+
51+
test "when url host contains subdomain returns subdomain" do
52+
tenant = "tenant"
53+
host = "#{tenant}.example.com"
54+
subdomain = AshPhoenix.Helpers.get_subdomain(host, TestEndpoint)
55+
56+
assert subdomain == tenant
57+
end
58+
59+
test "when Plug.Conn host contains subdomain returns subdomain" do
60+
tenant = "tenant"
61+
conn = %Plug.Conn{host: "#{tenant}.example.com"}
62+
subdomain = AshPhoenix.Helpers.get_subdomain(conn, TestEndpoint)
63+
64+
assert subdomain == tenant
65+
end
66+
67+
test "for Phoenix.Socket returns subdomain" do
68+
tenant = "tenant"
69+
socket = %Phoenix.Socket{endpoint: TestEndpoint}
70+
url = "https://#{tenant}.example.com"
71+
subdomain = AshPhoenix.Helpers.get_subdomain(socket, url)
72+
73+
assert subdomain == tenant
74+
end
75+
end
76+
end

0 commit comments

Comments
 (0)