Cleanup UX and fix a bunch of TODOs (#2641)

This PR cleans up a lot of TODO and some issues I've discovered while
fixing them, there are _a few_ UI changes.

We show `(you)` next to your name on the actor view page, where
`Profile` link goes from the dropdown menu:
<img width="1728" alt="Screenshot 2023-11-13 at 19 05 35"
src="https://github.com/firezone/firezone/assets/1877644/f52b2531-e3be-4d3a-a587-4f9f54ca2c49">

Relays were way behind Gateways in terms of view code, so I changed them
to be exactly the same:
<img width="1728" alt="Screenshot 2023-11-13 at 18 54 39"
src="https://github.com/firezone/firezone/assets/1877644/a9f0905d-80d2-4e91-a744-c4baf7ad4a7c">

We also show authorizations on the Actor page because previously to find
"what this user did" you had to go through all user clients
individually:
<img width="1728" alt="Screenshot 2023-11-13 at 18 54 27"
src="https://github.com/firezone/firezone/assets/1877644/02ada445-e175-427e-99de-f9fa5bdd5aab">

I've noticed there is some confusion around sign-in slugs so I added a
home page where you can use ID or slug to get the in link (not all the
clients will know you need to put that in the URL) and recently used
accounts:
<img width="1728" alt="Screenshot 2023-11-13 at 18 54 06"
src="https://github.com/firezone/firezone/assets/1877644/ccfb9198-ed1f-4b3e-a26f-b76bab24243c">

Buttons to copy the code are more visible now, I've used our accent
color but am open to better ideas:
<img width="1728" alt="Screenshot 2023-11-13 at 19 10 29"
src="https://github.com/firezone/firezone/assets/1877644/a2c0658e-1003-409b-b5ad-d5d3ade60a10">

When code is copied it's also more visible:
<img width="699" alt="Screenshot 2023-11-13 at 19 11 41"
src="https://github.com/firezone/firezone/assets/1877644/62e793d2-d760-4aa7-9a42-92a6bbfcbf52">

We also do not redirect from that page automatically, but the large
button becomes green with the text changed:
<img width="660" alt="Screenshot 2023-11-13 at 19 12 11"
src="https://github.com/firezone/firezone/assets/1877644/780dcde3-8018-4405-91e5-984288431ec1">
This commit is contained in:
Andrew Dryga
2023-11-14 13:02:21 -06:00
committed by GitHub
parent c4a44f44fc
commit 33ab23b636
105 changed files with 1327 additions and 622 deletions

View File

@@ -65,7 +65,7 @@ Now you can verify that it's working by connecting to a websocket:
# Note: The token value below is an example. The token value you will need is generated and printed out when seeding the database, as described earlier in the document.
export GATEWAY_TOKEN_FROM_SEEDS="SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAJXr4emIAWIAAVGA.jz0s-NohxgdAXeRMjIQ9kLBOyd7CmKXWi2FHY-Op8GM"
websocat --header="User-Agent: iOS/12.7 (iPhone) connlib/0.7.412" "ws://127.0.0.1:13000/gateway/websocket?token=${GATEWAY_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name_suffix=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
websocat --header="User-Agent: iOS/12.7 (iPhone) connlib/0.7.412" "ws://127.0.0.1:13000/gateway/websocket?token=${GATEWAY_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
# After this you need to join the `gateway` topic.
# For details on this structure see https://hexdocs.pm/phoenix/Phoenix.Socket.Message.html
@@ -107,10 +107,10 @@ Now you can verify that it's working by connecting to a websocket:
# Panel will only accept token if it's coming with this User-Agent header and from IP 172.28.0.1
export CLIENT_USER_AGENT="iOS/12.5 (iPhone) connlib/0.7.412"
websocat --header="User-Agent: ${CLIENT_USER_AGENT}" "ws://127.0.0.1:8081/client/websocket?token=${CLIENT_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name_suffix=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
websocat --header="User-Agent: ${CLIENT_USER_AGENT}" "ws://127.0.0.1:8081/client/websocket?token=${CLIENT_TOKEN_FROM_SEEDS}&external_id=thisisrandomandpersistent&name=kkX1&public_key=kceI60D6PrwOIiGoVz6hD7VYCgD1H57IVQlPJTTieUE="
# Here is what you will see in docker logs firezone-api-1
# firezone-api-1 | {"domain":["elixir"],"erl_level":"info","logging.googleapis.com/sourceLocation":{"file":"lib/phoenix/logger.ex","line":306,"function":"Elixir.Phoenix.Logger.phoenix_socket_connected/4"},"message":"CONNECTED TO API.Client.Socket in 83ms\n Transport: :websocket\n Serializer: Phoenix.Socket.V1.JSONSerializer\n Parameters: %{\"external_id\" => \"thisisrandomandpersistent\", \"name_suffix\" => \"kkX1\", \"public_key\" => \"[FILTERED]\", \"token\" => \"[FILTERED]\"}","severity":"INFO","time":"2023-06-23T21:01:49.566Z"}
# firezone-api-1 | {"domain":["elixir"],"erl_level":"info","logging.googleapis.com/sourceLocation":{"file":"lib/phoenix/logger.ex","line":306,"function":"Elixir.Phoenix.Logger.phoenix_socket_connected/4"},"message":"CONNECTED TO API.Client.Socket in 83ms\n Transport: :websocket\n Serializer: Phoenix.Socket.V1.JSONSerializer\n Parameters: %{\"external_id\" => \"thisisrandomandpersistent\", \"name\" => \"kkX1\", \"public_key\" => \"[FILTERED]\", \"token\" => \"[FILTERED]\"}","severity":"INFO","time":"2023-06-23T21:01:49.566Z"}
# After this you need to join the `client` topic and pass a `stamp_secret` in the payload.
# For details on this structure see https://hexdocs.pm/phoenix/Phoenix.Socket.Message.html

View File

@@ -28,7 +28,7 @@ defmodule API.Gateway.Socket do
attrs =
attrs
|> Map.take(~w[external_id name_suffix public_key])
|> Map.take(~w[external_id name public_key])
|> Map.put("last_seen_user_agent", user_agent)
|> Map.put("last_seen_remote_ip", real_ip)
|> Map.put("last_seen_remote_ip_location_region", location_region)

View File

@@ -3,6 +3,15 @@ defmodule Domain.Accounts do
alias Domain.Auth
alias Domain.Accounts.{Authorizer, Account}
def list_accounts_by_ids(ids) do
if Enum.all?(ids, &Validator.valid_uuid?/1) do
Account.Query.by_id({:in, ids})
|> Repo.list()
else
{:ok, []}
end
end
def fetch_account_by_id(id, %Auth.Subject{} = subject) do
with :ok <- Auth.ensure_has_permissions(subject, Authorizer.view_accounts_permission()),
true <- Validator.valid_uuid?(id) do

View File

@@ -6,7 +6,13 @@ defmodule Domain.Accounts.Account.Query do
# |> where([account: account], is_nil(account.deleted_at))
end
def by_id(queryable \\ all(), id) do
def by_id(queryable \\ all(), id)
def by_id(queryable, {:in, ids}) do
where(queryable, [account: account], account.id in ^ids)
end
def by_id(queryable, id) do
where(queryable, [account: account], account.id == ^id)
end

View File

@@ -1,6 +1,6 @@
defmodule Domain.Flows do
alias Domain.{Repo, Validator}
alias Domain.{Auth, Accounts, Clients, Gateways, Resources, Policies}
alias Domain.{Auth, Accounts, Actors, Clients, Gateways, Resources, Policies}
alias Domain.Flows.{Authorizer, Flow, Activity}
require Ecto.Query
@@ -84,6 +84,11 @@ defmodule Domain.Flows do
|> list_flows(subject, opts)
end
def list_flows_for(%Actors.Actor{} = actor, %Auth.Subject{} = subject, opts) do
Flow.Query.by_actor_id(actor.id)
|> list_flows(subject, opts)
end
def list_flows_for(%Gateways.Gateway{} = gateway, %Auth.Subject{} = subject, opts) do
Flow.Query.by_gateway_id(gateway.id)
|> list_flows(subject, opts)

View File

@@ -25,7 +25,19 @@ defmodule Domain.Flows.Flow.Query do
where(queryable, [flows: flows], flows.client_id == ^client_id)
end
def by_actor_id(queryable \\ all(), actor_id) do
queryable
|> with_joined_client()
|> where([client: client], client.actor_id == ^actor_id)
end
def by_gateway_id(queryable \\ all(), gateway_id) do
where(queryable, [flows: flows], flows.gateway_id == ^gateway_id)
end
def with_joined_client(queryable \\ all()) do
with_named_binding(queryable, :client, fn queryable, binding ->
join(queryable, :inner, [flows: flows], client in assoc(flows, ^binding), as: ^binding)
end)
end
end

View File

@@ -4,8 +4,7 @@ defmodule Domain.Gateways.Gateway do
schema "gateways" do
field :external_id, :string
# TODO: hostname
field :name_suffix, :string
field :name, :string
field :public_key, :string

View File

@@ -3,7 +3,7 @@ defmodule Domain.Gateways.Gateway.Changeset do
alias Domain.Version
alias Domain.Gateways
@upsert_fields ~w[external_id name_suffix public_key
@upsert_fields ~w[external_id name public_key
last_seen_user_agent
last_seen_remote_ip
last_seen_remote_ip_location_region
@@ -20,8 +20,8 @@ defmodule Domain.Gateways.Gateway.Changeset do
last_seen_version
last_seen_at
updated_at]a
@update_fields ~w[name_suffix]a
@required_fields ~w[external_id name_suffix public_key
@update_fields ~w[name]a
@required_fields ~w[external_id name public_key
last_seen_user_agent last_seen_remote_ip]a
# WireGuard base64-encoded string length
@@ -35,7 +35,7 @@ defmodule Domain.Gateways.Gateway.Changeset do
def upsert(%Gateways.Token{} = token, attrs) do
%Gateways.Gateway{}
|> cast(attrs, @upsert_fields)
|> put_default_value(:name_suffix, fn ->
|> put_default_value(:name, fn ->
Domain.Crypto.random_token(5, encoder: :user_friendly)
end)
|> changeset()
@@ -43,7 +43,6 @@ defmodule Domain.Gateways.Gateway.Changeset do
|> validate_base64(:public_key)
|> validate_length(:public_key, is: @key_length)
|> unique_constraint(:public_key, name: :gateways_account_id_public_key_index)
|> unique_constraint(:name_suffix, name: :gateways_account_id_group_id_name_suffix_index)
|> unique_constraint(:ipv4)
|> unique_constraint(:ipv6)
|> put_change(:last_seen_at, DateTime.utc_now())
@@ -76,9 +75,9 @@ defmodule Domain.Gateways.Gateway.Changeset do
defp changeset(changeset) do
changeset
|> trim_change(:name_suffix)
|> validate_length(:name_suffix, min: 1, max: 8)
|> unique_constraint(:name_suffix, name: :gateways_group_id_name_suffix_index)
|> trim_change(:name)
|> validate_length(:name, min: 1, max: 8)
|> unique_constraint(:name, name: :gateways_group_id_name_index)
|> unique_constraint([:public_key])
|> unique_constraint(:external_id)
end

View File

@@ -2,8 +2,7 @@ defmodule Domain.Gateways.Group do
use Domain, :schema
schema "gateway_groups" do
# TODO: name
field :name_prefix, :string
field :name, :string
belongs_to :account, Domain.Accounts.Account
has_many :gateways, Domain.Gateways.Gateway, foreign_key: :group_id, where: [deleted_at: nil]

View File

@@ -3,7 +3,7 @@ defmodule Domain.Gateways.Group.Changeset do
alias Domain.{Auth, Accounts}
alias Domain.Gateways
@fields ~w[name_prefix]a
@fields ~w[name]a
def create(%Accounts.Account{} = account, attrs, %Auth.Subject{} = subject) do
%Gateways.Group{account: account}
@@ -35,11 +35,11 @@ defmodule Domain.Gateways.Group.Changeset do
defp changeset(%Gateways.Group{} = group, attrs) do
group
|> cast(attrs, @fields)
|> trim_change(:name_prefix)
|> put_default_value(:name_prefix, &Domain.NameGenerator.generate/0)
|> trim_change(:name)
|> put_default_value(:name, &Domain.NameGenerator.generate/0)
|> validate_required(@fields)
|> validate_length(:name_prefix, min: 1, max: 64)
|> unique_constraint(:name_prefix, name: :gateway_groups_account_id_name_prefix_index)
|> validate_length(:name, min: 1, max: 64)
|> unique_constraint(:name, name: :gateway_groups_account_id_name_index)
end
def delete(%Gateways.Group{} = group) do

View File

@@ -4,8 +4,8 @@ defmodule Domain.Policies.Policy do
schema "policies" do
field :description, :string
belongs_to :actor_group, Domain.Actors.Group
belongs_to :resource, Domain.Resources.Resource
belongs_to :actor_group, Domain.Actors.Group, where: [deleted_at: nil]
belongs_to :resource, Domain.Resources.Resource, where: [deleted_at: nil]
belongs_to :account, Domain.Accounts.Account
field :created_by, Ecto.Enum, values: ~w[identity]a

View File

@@ -4,6 +4,10 @@ defmodule Domain.Policies.Policy.Query do
def all do
from(policies in Domain.Policies.Policy, as: :policies)
|> where([policies: policies], is_nil(policies.deleted_at))
|> with_joined_actor_group()
|> where([actor_group: actor_group], is_nil(actor_group.deleted_at))
|> with_joined_resource()
|> where([resource: resource], is_nil(resource.deleted_at))
end
def by_id(queryable \\ all(), id) do
@@ -45,6 +49,14 @@ defmodule Domain.Policies.Policy.Query do
end)
end
def with_joined_resource(queryable \\ all()) do
with_named_binding(queryable, :resource, fn queryable, binding ->
join(queryable, :inner, [policies: policies], resource in assoc(policies, ^binding),
as: ^binding
)
end)
end
def with_joined_memberships(queryable \\ all()) do
queryable
|> with_joined_actor_group()

View File

@@ -0,0 +1,12 @@
defmodule Domain.Repo.Migrations.RenameGatewayGroupsNamePrefix do
use Ecto.Migration
def change do
rename(table(:gateway_groups), :name_prefix, to: :name)
execute("""
ALTER INDEX gateway_groups_account_id_name_prefix_index
RENAME TO gateway_groups_account_id_name_index
""")
end
end

View File

@@ -0,0 +1,14 @@
defmodule Domain.Repo.Migrations.RenameGatewayNameSuffixToName do
use Ecto.Migration
def change do
rename(table(:gateways), :name_suffix, to: :name)
drop(
index(:gateways, [:account_id, :group_id, :name_suffix],
unique: true,
where: "deleted_at IS NULL"
)
)
end
end

View File

@@ -342,7 +342,7 @@ IO.puts("")
gateway_group =
account
|> Gateways.Group.Changeset.create(
%{name_prefix: "mycro-aws-gws", tokens: [%{}]},
%{name: "mycro-aws-gws", tokens: [%{}]},
admin_subject
)
|> Repo.insert!()
@@ -359,13 +359,13 @@ gateway_group_token =
)
IO.puts("Created gateway groups:")
IO.puts(" #{gateway_group.name_prefix} token: #{Gateways.encode_token!(gateway_group_token)}")
IO.puts(" #{gateway_group.name} token: #{Gateways.encode_token!(gateway_group_token)}")
IO.puts("")
{:ok, gateway1} =
Gateways.upsert_gateway(gateway_group_token, %{
external_id: Ecto.UUID.generate(),
name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}}
@@ -374,7 +374,7 @@ IO.puts("")
{:ok, gateway2} =
Gateways.upsert_gateway(gateway_group_token, %{
external_id: Ecto.UUID.generate(),
name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62}}
@@ -384,7 +384,7 @@ for i <- 1..10 do
{:ok, _gateway} =
Gateways.upsert_gateway(gateway_group_token, %{
external_id: Ecto.UUID.generate(),
name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
last_seen_remote_ip: %Postgrex.INET{address: {164, 112, 78, 62 + i}}
@@ -392,14 +392,14 @@ for i <- 1..10 do
end
IO.puts("Created gateways:")
gateway_name = "#{gateway_group.name_prefix}-#{gateway1.name_suffix}"
gateway_name = "#{gateway_group.name}-#{gateway1.name}"
IO.puts(" #{gateway_name}:")
IO.puts(" External UUID: #{gateway1.external_id}")
IO.puts(" Public Key: #{gateway1.public_key}")
IO.puts(" IPv4: #{gateway1.ipv4} IPv6: #{gateway1.ipv6}")
IO.puts("")
gateway_name = "#{gateway_group.name_prefix}-#{gateway2.name_suffix}"
gateway_name = "#{gateway_group.name}-#{gateway2.name}"
IO.puts(" #{gateway_name}:")
IO.puts(" External UUID: #{gateway1.external_id}")
IO.puts(" Public Key: #{gateway2.public_key}")

View File

@@ -273,6 +273,7 @@ defmodule Domain.FlowsTest do
describe "list_flows_for/2" do
test "returns empty list when there are no flows", %{
actor: actor,
client: client,
gateway: gateway,
resource: resource,
@@ -281,11 +282,13 @@ defmodule Domain.FlowsTest do
} do
assert list_flows_for(policy, subject) == {:ok, []}
assert list_flows_for(resource, subject) == {:ok, []}
assert list_flows_for(actor, subject) == {:ok, []}
assert list_flows_for(client, subject) == {:ok, []}
assert list_flows_for(gateway, subject) == {:ok, []}
end
test "does not list flows from other accounts", %{
actor: actor,
client: client,
gateway: gateway,
resource: resource,
@@ -296,12 +299,14 @@ defmodule Domain.FlowsTest do
assert list_flows_for(policy, subject) == {:ok, []}
assert list_flows_for(resource, subject) == {:ok, []}
assert list_flows_for(actor, subject) == {:ok, []}
assert list_flows_for(client, subject) == {:ok, []}
assert list_flows_for(gateway, subject) == {:ok, []}
end
test "returns all authorized flows", %{
test "returns all authorized flows for a given entity", %{
account: account,
actor: actor,
client: client,
gateway: gateway,
resource: resource,
@@ -320,12 +325,33 @@ defmodule Domain.FlowsTest do
assert list_flows_for(policy, subject) == {:ok, [flow]}
assert list_flows_for(resource, subject) == {:ok, [flow]}
assert list_flows_for(actor, subject) == {:ok, [flow]}
assert list_flows_for(client, subject) == {:ok, [flow]}
assert list_flows_for(gateway, subject) == {:ok, [flow]}
end
test "does not return authorized flow of other entities", %{
account: account,
actor: actor,
client: client,
gateway: gateway,
resource: resource,
policy: policy,
subject: subject
} do
other_client = Fixtures.Clients.create_client(account: account)
Fixtures.Flows.create_flow(account: account, client: other_client, subject: subject)
assert list_flows_for(policy, subject) == {:ok, []}
assert list_flows_for(resource, subject) == {:ok, []}
assert list_flows_for(actor, subject) == {:ok, []}
assert list_flows_for(client, subject) == {:ok, []}
assert list_flows_for(gateway, subject) == {:ok, []}
end
test "returns error when subject has no permission to view flows", %{
client: client,
actor: actor,
gateway: gateway,
resource: resource,
policy: policy,
@@ -340,6 +366,7 @@ defmodule Domain.FlowsTest do
assert list_flows_for(policy, subject) == expected_error
assert list_flows_for(resource, subject) == expected_error
assert list_flows_for(client, subject) == expected_error
assert list_flows_for(actor, subject) == expected_error
assert list_flows_for(gateway, subject) == expected_error
end
end

View File

@@ -121,7 +121,7 @@ defmodule Domain.GatewaysTest do
describe "new_group/0" do
test "returns group changeset" do
assert %Ecto.Changeset{data: %Gateways.Group{}, changes: changes} = new_group()
assert Map.has_key?(changes, :name_prefix)
assert Map.has_key?(changes, :name)
assert Enum.count(changes) == 1
end
end
@@ -134,31 +134,31 @@ defmodule Domain.GatewaysTest do
test "returns error on invalid attrs", %{account: account, subject: subject} do
attrs = %{
name_prefix: String.duplicate("A", 65)
name: String.duplicate("A", 65)
}
assert {:error, changeset} = create_group(attrs, subject)
assert errors_on(changeset) == %{
tokens: ["can't be blank"],
name_prefix: ["should be at most 64 character(s)"]
name: ["should be at most 64 character(s)"]
}
Fixtures.Gateways.create_group(account: account, name_prefix: "foo")
attrs = %{name_prefix: "foo", tokens: [%{}]}
Fixtures.Gateways.create_group(account: account, name: "foo")
attrs = %{name: "foo", tokens: [%{}]}
assert {:error, changeset} = create_group(attrs, subject)
assert "has already been taken" in errors_on(changeset).name_prefix
assert "has already been taken" in errors_on(changeset).name
end
test "creates a group", %{subject: subject} do
attrs = %{
name_prefix: "foo",
name: "foo",
tokens: [%{}]
}
assert {:ok, group} = create_group(attrs, subject)
assert group.id
assert group.name_prefix == "foo"
assert group.name == "foo"
assert group.created_by == :identity
assert group.created_by_identity_id == subject.identity.id
@@ -190,7 +190,7 @@ defmodule Domain.GatewaysTest do
assert changeset = change_group(group, group_attrs)
assert changeset.valid?
assert changeset.changes == %{name_prefix: group_attrs.name_prefix}
assert changeset.changes == %{name: group_attrs.name}
end
end
@@ -199,41 +199,41 @@ defmodule Domain.GatewaysTest do
subject: subject
} do
group = Fixtures.Gateways.create_group()
attrs = %{name_prefix: nil}
attrs = %{name: nil}
assert {:error, changeset} = update_group(group, attrs, subject)
assert errors_on(changeset) == %{name_prefix: ["can't be blank"]}
assert errors_on(changeset) == %{name: ["can't be blank"]}
end
test "returns error on invalid attrs", %{account: account, subject: subject} do
group = Fixtures.Gateways.create_group(account: account)
attrs = %{
name_prefix: String.duplicate("A", 65)
name: String.duplicate("A", 65)
}
assert {:error, changeset} = update_group(group, attrs, subject)
assert errors_on(changeset) == %{
name_prefix: ["should be at most 64 character(s)"]
name: ["should be at most 64 character(s)"]
}
Fixtures.Gateways.create_group(account: account, name_prefix: "foo")
attrs = %{name_prefix: "foo"}
Fixtures.Gateways.create_group(account: account, name: "foo")
attrs = %{name: "foo"}
assert {:error, changeset} = update_group(group, attrs, subject)
assert "has already been taken" in errors_on(changeset).name_prefix
assert "has already been taken" in errors_on(changeset).name
end
test "updates a group", %{account: account, subject: subject} do
group = Fixtures.Gateways.create_group(account: account)
attrs = %{
name_prefix: "foo"
name: "foo"
}
assert {:ok, group} = update_group(group, attrs, subject)
assert group.name_prefix == "foo"
assert group.name == "foo"
end
test "returns error when subject has no permission to manage groups", %{
@@ -536,7 +536,7 @@ defmodule Domain.GatewaysTest do
assert changeset = change_gateway(gateway, gateway_attrs)
assert %Ecto.Changeset{data: %Domain.Gateways.Gateway{}} = changeset
assert changeset.changes == %{name_suffix: gateway_attrs.name_suffix}
assert changeset.changes == %{name: gateway_attrs.name}
end
end
@@ -585,7 +585,7 @@ defmodule Domain.GatewaysTest do
assert {:ok, gateway} = upsert_gateway(token, attrs)
assert gateway.name_suffix
assert gateway.name
assert gateway.public_key == attrs.public_key
assert gateway.token_id == token.id
@@ -627,7 +627,7 @@ defmodule Domain.GatewaysTest do
assert Repo.aggregate(Gateways.Gateway, :count, :id) == 1
assert updated_gateway.name_suffix
assert updated_gateway.name
assert updated_gateway.last_seen_remote_ip.address == attrs.last_seen_remote_ip
assert updated_gateway.last_seen_remote_ip != gateway.last_seen_remote_ip
assert updated_gateway.last_seen_user_agent == attrs.last_seen_user_agent
@@ -710,11 +710,11 @@ defmodule Domain.GatewaysTest do
describe "update_gateway/3" do
test "updates gateways", %{account: account, subject: subject} do
gateway = Fixtures.Gateways.create_gateway(account: account)
attrs = %{name_suffix: "Foo"}
attrs = %{name: "Foo"}
assert {:ok, gateway} = update_gateway(gateway, attrs, subject)
assert gateway.name_suffix == attrs.name_suffix
assert gateway.name == attrs.name
end
test "does not allow to reset required fields to empty values", %{
@@ -722,24 +722,24 @@ defmodule Domain.GatewaysTest do
subject: subject
} do
gateway = Fixtures.Gateways.create_gateway(account: account)
attrs = %{name_suffix: nil}
attrs = %{name: nil}
assert {:error, changeset} = update_gateway(gateway, attrs, subject)
assert errors_on(changeset) == %{name_suffix: ["can't be blank"]}
assert errors_on(changeset) == %{name: ["can't be blank"]}
end
test "returns error on invalid attrs", %{account: account, subject: subject} do
gateway = Fixtures.Gateways.create_gateway(account: account)
attrs = %{
name_suffix: String.duplicate("a", 256)
name: String.duplicate("a", 256)
}
assert {:error, changeset} = update_gateway(gateway, attrs, subject)
assert errors_on(changeset) == %{
name_suffix: ["should be at most 8 character(s)"]
name: ["should be at most 8 character(s)"]
}
end
@@ -749,7 +749,7 @@ defmodule Domain.GatewaysTest do
} do
gateway = Fixtures.Gateways.create_gateway(account: account)
fields = Gateways.Gateway.__schema__(:fields) -- [:name_suffix]
fields = Gateways.Gateway.__schema__(:fields) -- [:name]
value = -1
for field <- fields do

View File

@@ -91,6 +91,38 @@ defmodule Domain.PoliciesTest do
assert list_policies(subject) == {:ok, []}
end
test "does not list policies for deleted resources", %{account: account, subject: subject} do
resource =
Fixtures.Resources.create_resource(account: account)
|> Fixtures.Resources.delete_resource()
actor_group = Fixtures.Actors.create_group(account: account)
Fixtures.Policies.create_policy(
account: account,
resource: resource,
actor_group: actor_group
)
assert list_policies(subject) == {:ok, []}
end
test "does not list policies for deleted actor groups", %{account: account, subject: subject} do
resource = Fixtures.Resources.create_resource(account: account)
actor_group =
Fixtures.Actors.create_group(account: account)
|> Fixtures.Actors.delete_group()
Fixtures.Policies.create_policy(
account: account,
resource: resource,
actor_group: actor_group
)
assert list_policies(subject) == {:ok, []}
end
test "returns all policies for account admin subject", %{account: account} do
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)

View File

@@ -4,7 +4,7 @@ defmodule Domain.Fixtures.Gateways do
def group_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
name_prefix: "group-#{unique_integer()}",
name: "group-#{unique_integer()}",
tokens: [%{}]
})
end
@@ -71,7 +71,7 @@ defmodule Domain.Fixtures.Gateways do
def gateway_attrs(attrs \\ %{}) do
Enum.into(attrs, %{
external_id: Ecto.UUID.generate(),
name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
name: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
public_key: unique_public_key(),
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}},

View File

@@ -13,14 +13,22 @@ Hooks.Copy = {
let doc = new DOMParser().parseFromString(inner_html, "text/html");
let text = doc.documentElement.textContent;
let cl = ev.currentTarget.querySelector("[data-icon]").classList
let content = ev.currentTarget.querySelector("[data-content]")
let icon_cl = ev.currentTarget.querySelector("[data-icon]").classList
navigator.clipboard.writeText(text).then(() => {
cl.add("hero-clipboard-document-check");
cl.add("text-green-500");
cl.remove("hero-clipboard-document");
cl.remove("text-gray-500");
})
icon_cl.add("hero-clipboard-document-check");
icon_cl.add("text-green-500");
icon_cl.remove("hero-clipboard-document");
content.innerHTML = "Copied"
});
setTimeout(() => {
icon_cl.remove("hero-clipboard-document-check");
icon_cl.remove("text-green-500");
icon_cl.add("hero-clipboard-document");
content.innerHTML = "Copy"
}, 2000);
});
},
}

View File

@@ -2,6 +2,18 @@ defmodule Web.Auth do
use Web, :verified_routes
alias Domain.{Auth, Accounts}
# This is the cookie which will store recent account ids
# that the user has signed in to.
@remember_me_cookie_name "fz_recent_account_ids"
@remember_me_cookie_options [
sign: true,
max_age: 365 * 24 * 60 * 60,
same_site: "Lax",
secure: true,
http_only: true
]
@remember_last_account_ids 5
def signed_in_path(%Auth.Subject{actor: %{type: :account_admin_user}} = subject) do
~p"/#{subject.account}/sites"
end
@@ -132,6 +144,37 @@ defmodule Web.Auth do
|> Plug.Conn.put_session(:preferred_locale, preferred_locale)
end
###########################
## Controller Helpers
###########################
def list_recent_account_ids(conn) do
conn = Plug.Conn.fetch_cookies(conn, signed: [@remember_me_cookie_name])
if recent_account_ids = Map.get(conn.cookies, @remember_me_cookie_name) do
{:ok, :erlang.binary_to_term(recent_account_ids, [:safe]), conn}
else
{:ok, [], conn}
end
end
def update_recent_account_ids(conn, callback) when is_function(callback, 1) do
{:ok, recent_account_ids, conn} = list_recent_account_ids(conn)
recent_account_ids =
recent_account_ids
|> callback.()
|> Enum.take(@remember_last_account_ids)
|> :erlang.term_to_binary()
Plug.Conn.put_resp_cookie(
conn,
@remember_me_cookie_name,
recent_account_ids,
@remember_me_cookie_options
)
end
###########################
## Plugs
###########################

View File

@@ -70,12 +70,23 @@ defmodule Web.CoreComponents do
data-copy
phx-no-format
><%= render_slot(@inner_block) %></code>
<.icon name="hero-clipboard-document" data-icon class={~w[
<span class={~w[
absolute bottom-1 right-1
h-5 w-5
text-gray-400
transition
text-gray-500 group-hover:text-white
]} />
cursor-pointer
rounded
px-2
focus:ring-4 focus:outline-none
text-white
bg-accent-400
hover:bg-accent-500
focus:ring-accent-300
]}>
<.icon name="hero-clipboard-document" data-icon class="h-4 w-4" />
<span data-content>Copy</span>
</span>
</div>
"""
end
@@ -486,6 +497,27 @@ defmodule Web.CoreComponents do
"""
end
def icon(%{name: "spinner"} = assigns) do
~H"""
<svg
class={["inline-block", @class]}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
{@rest}
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
</circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>
"""
end
@doc """
Renders Gravatar img tag.
"""
@@ -717,6 +749,40 @@ defmodule Web.CoreComponents do
"""
end
attr :navigate, :string, required: true
attr :connected?, :boolean, required: true
attr :type, :string, required: true
def initial_connection_status(assigns) do
~H"""
<.link
class={[
"mx-4 my-6 h-8",
"flex items-center justify-center",
"font-medium text-sm text-white",
"rounded-full",
(@connected? && "bg-green-500") || "bg-orange-400 cursor-progress"
]}
navigate={@navigate}
{
if @connected? do
%{}
else
%{"data-confirm" => "Do you want to skip waiting for #{@type} to be connected?"}
end
}
>
<span :if={not @connected?}>
<.icon name="spinner" class="animate-spin h-3.5 w-3.5 mr-1" /> Waiting for connection...
</span>
<span :if={@connected?}>
<.icon name="hero-check" class="h-3.5 w-3.5" /> Connected, click to continue
</span>
</.link>
"""
end
@doc """
Renders creation timestamp and entity.
"""

View File

@@ -3,17 +3,6 @@ defmodule Web.AuthController do
alias Web.Auth
alias Domain.Auth.Adapters.OpenIDConnect
# This is the cookie which will store recent account ids
# that the user has signed in to.
@remember_me_cookie_name "fz_recent_account_ids"
@remember_me_cookie_options [
sign: true,
max_age: 365 * 24 * 60 * 60,
same_site: "Lax",
secure: true,
http_only: true
]
# This is the cookie which will be used to store the
# state and code verifier for OpenID Connect IdP's
@state_cookie_key_prefix "fz_auth_state_"
@@ -306,7 +295,6 @@ defmodule Web.AuthController do
Domain.Auth.sign_out(subject.identity, url(~p"/#{account_id_or_slug}?#{redirect_params}"))
conn
|> delete_recent_account()
|> Auth.sign_out()
|> redirect(external: redirect_url)
end
@@ -319,42 +307,12 @@ defmodule Web.AuthController do
|> redirect(to: ~p"/#{account_id_or_slug}?#{redirect_params}")
end
defp delete_recent_account(%{assigns: %{subject: subject}} = conn) do
update_recent_accounts(conn, fn recent_account_ids ->
recent_account_ids -- [subject.account.id]
end)
end
defp persist_recent_account(conn, %Domain.Accounts.Account{} = account) do
update_recent_accounts(conn, fn recent_account_ids ->
Auth.update_recent_account_ids(conn, fn recent_account_ids ->
[account.id] ++ recent_account_ids
end)
end
defp update_recent_accounts(conn, callback) when is_function(callback, 1) do
conn = fetch_cookies(conn, signed: [@remember_me_cookie_name])
recent_account_ids =
if recent_account_ids = Map.get(conn.cookies, @remember_me_cookie_name) do
:erlang.binary_to_term(recent_account_ids, [:safe])
else
[]
end
recent_account_ids =
recent_account_ids
|> callback.()
|> Enum.take(5)
|> :erlang.term_to_binary()
put_resp_cookie(
conn,
@remember_me_cookie_name,
recent_account_ids,
@remember_me_cookie_options
)
end
defp take_non_empty_params(map, keys) do
map |> Map.take(keys) |> Map.reject(fn {_key, value} -> value in ["", nil] end)
end

View File

@@ -0,0 +1,27 @@
defmodule Web.HomeController do
use Web, :controller
alias Domain.Accounts
def home(conn, _params) do
{accounts, conn} =
with {:ok, recent_account_ids, conn} <- Web.Auth.list_recent_account_ids(conn),
{:ok, accounts} <- Accounts.list_accounts_by_ids(recent_account_ids) do
conn =
Web.Auth.update_recent_account_ids(conn, fn _recent_account_ids ->
Enum.map(accounts, & &1.id)
end)
{accounts, conn}
else
_other -> {[], conn}
end
conn
|> put_layout(html: {Web.Layouts, :public})
|> render("home.html", accounts: accounts)
end
def redirect_to_sign_in(conn, %{"account_id_or_slug" => account_id_or_slug}) do
redirect(conn, to: ~p"/#{account_id_or_slug}")
end
end

View File

@@ -0,0 +1,81 @@
defmodule Web.HomeHTML do
use Web, :html
def home(assigns) do
~H"""
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0">
<.logo />
<div class="w-full col-span-6 mx-auto bg-white rounded shadow dark:bg-gray-800 md:mt-0 sm:max-w-lg xl:p-0">
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
<h1 class="text-xl text-center font-bold leading-tight tracking-tight text-gray-900 sm:text-2xl dark:text-white">
Welcome to Firezone
</h1>
<h3
:if={@accounts != []}
class="text-m font-bold leading-tight tracking-tight text-gray-900 sm:text-xl dark:text-white"
>
Recently used accounts
</h3>
<div :if={@accounts != []} class="space-y-3 items-center">
<.account_button :for={account <- @accounts} account={account} />
</div>
<.separator if={@accounts != []} />
<.form :let={f} for={%{}} action={~p"/"} class="space-y-4 lg:space-y-6">
<.input
field={f[:account_id_or_slug]}
type="text"
label="Account ID or Slug"
placeholder={~s|As shown in your "Welcome to Firezone" email|}
required
/>
<.button class="w-full">
Go to Sign In page
</.button>
</.form>
<p :if={Domain.Config.sign_up_enabled?()} class="py-2">
Don't have an account?
<a href={~p"/sign_up"} class="font-medium text-blue-600 hover:text-blue-500">
Sign up here.
</a>
</p>
</div>
</div>
</div>
</section>
"""
end
def account_button(assigns) do
~H"""
<a href={~p"/#{@account}"} class={~w[
w-full inline-flex items-center justify-center py-2.5 px-5
bg-white rounded
text-sm font-medium text-gray-900
focus:outline-none
border border-gray-200
hover:bg-gray-100 hover:text-gray-900
focus:z-10 focus:ring-4 focus:ring-gray-200
dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400
dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700]}>
<%= @account.name %>
</a>
"""
end
def separator(assigns) do
~H"""
<div class="flex items-center">
<div class="w-full h-0.5 bg-gray-200 dark:bg-gray-700"></div>
<div class="px-5 text-center text-gray-500 dark:text-gray-400">or</div>
<div class="w-full h-0.5 bg-gray-200 dark:bg-gray-700"></div>
</div>
"""
end
end

View File

@@ -1,7 +0,0 @@
defmodule Web.RedirectController do
use Web, :controller
def home(conn, _params) do
redirect(conn, external: "https://firezone.dev")
end
end

View File

@@ -75,7 +75,7 @@ defmodule Web.Actors.Edit do
attrs = map_actor_form_memberships_attr(attrs)
with {:ok, actor} <- Actors.update_actor(socket.assigns.actor, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/actors/#{actor}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/actors/#{actor}")
{:noreply, socket}
else
{:error, :unauthorized} ->

View File

@@ -79,7 +79,7 @@ defmodule Web.Actors.ServiceAccounts.New do
socket.assigns.subject
) do
socket =
redirect(socket,
push_navigate(socket,
to: ~p"/#{socket.assigns.account}/actors/service_accounts/#{actor}/new_identity"
)

View File

@@ -1,7 +1,7 @@
defmodule Web.Actors.Show do
use Web, :live_view
import Web.Actors.Components
alias Domain.Auth
alias Domain.{Auth, Flows}
alias Domain.Actors
def mount(%{"id" => id}, _session, socket) do
@@ -9,10 +9,21 @@ defmodule Web.Actors.Show do
Actors.fetch_actor_by_id(id, socket.assigns.subject,
preload: [
identities: [:provider, created_by_identity: [:actor]],
groups: [:provider]
groups: [:provider],
clients: []
]
),
{:ok, flows} <-
Flows.list_flows_for(actor, socket.assigns.subject,
preload: [gateway: [:group], client: [], policy: [:resource, :actor_group]]
) do
{:ok, assign(socket, actor: actor), temporary_assigns: [page_title: actor.name]}
{:ok,
assign(socket,
actor: actor,
flows: flows,
page_title: actor.name,
flow_activities_enabled?: Domain.Config.flow_activities_enabled?()
)}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
@@ -30,6 +41,7 @@ defmodule Web.Actors.Show do
<.section>
<:title>
<%= actor_type(@actor.type) %>: <span class="font-bold"><%= @actor.name %></span>
<span :if={@actor.id == @subject.actor.id} class="text-gray-400">(you)</span>
</:title>
<:action>
<.edit_button navigate={~p"/#{@account}/actors/#{@actor}/edit"}>
@@ -145,6 +157,80 @@ defmodule Web.Actors.Show do
</:content>
</.section>
<.section>
<:title>Clients</:title>
<:content>
<.table id="clients" rows={@actor.clients} row_id={&"client-#{&1.id}"}>
<:col :let={client} label="NAME">
<.link
navigate={~p"/#{@account}/clients/#{client.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= client.name %>
</.link>
</:col>
<:col :let={client} label="STATUS">
<.connection_status schema={client} />
</:col>
<:empty>
<div class="text-center text-slate-500 p-4">No clients to display</div>
<div class="text-center text-slate-500 mb-4">
Clients are created automatically when user connects to a resource.
</div>
</:empty>
</.table>
</:content>
</.section>
<.section>
<:title>Authorizations</:title>
<:content>
<.table id="flows" rows={@flows} row_id={&"flows-#{&1.id}"}>
<:col :let={flow} label="AUTHORIZED AT">
<.relative_datetime datetime={flow.inserted_at} />
</:col>
<:col :let={flow} label="EXPIRES AT">
<.relative_datetime datetime={flow.expires_at} />
</:col>
<:col :let={flow} label="POLICY">
<.link
navigate={~p"/#{@account}/policies/#{flow.policy_id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<Web.Policies.Components.policy_name policy={flow.policy} />
</.link>
</:col>
<:col :let={flow} label="CLIENT (IP)">
<.link navigate={~p"/#{@account}/clients/#{flow.client_id}"} class={link_style()}>
<%= flow.client.name %>
</.link>
(<%= flow.client_remote_ip %>)
</:col>
<:col :let={flow} label="GATEWAY (IP)">
<.link
navigate={~p"/#{@account}/gateways/#{flow.gateway_id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= flow.gateway.group.name %>-<%= flow.gateway.name %>
</.link>
(<%= flow.gateway_remote_ip %>)
</:col>
<:col :let={flow} :if={@flow_activities_enabled?} label="ACTIVITY">
<.link
navigate={~p"/#{@account}/flows/#{flow.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
Show
</.link>
</:col>
<:empty>
<div class="text-center text-slate-500 p-4">No authorizations to display</div>
</:empty>
</.table>
</:content>
</.section>
<.danger_zone>
<:action>
<.delete_button
@@ -184,7 +270,7 @@ defmodule Web.Actors.Show do
def handle_event("delete", _params, socket) do
with {:ok, _actor} <- Actors.delete_actor(socket.assigns.actor, socket.assigns.subject) do
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/actors")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/actors")}
else
{:error, :cant_delete_the_last_admin} ->
{:noreply, put_flash(socket, :error, "You can't delete the last admin of an account.")}
@@ -196,7 +282,8 @@ defmodule Web.Actors.Show do
actor = %{
actor
| identities: socket.assigns.actor.identities,
groups: socket.assigns.actor.groups
groups: socket.assigns.actor.groups,
clients: socket.assigns.actor.clients
}
socket =
@@ -217,7 +304,8 @@ defmodule Web.Actors.Show do
actor = %{
actor
| identities: socket.assigns.actor.identities,
groups: socket.assigns.actor.groups
groups: socket.assigns.actor.groups,
clients: socket.assigns.actor.clients
}
socket =
@@ -235,11 +323,16 @@ defmodule Web.Actors.Show do
{:ok, actor} =
Actors.fetch_actor_by_id(socket.assigns.actor.id, socket.assigns.subject,
preload: [
identities: [:provider, created_by_identity: [:actor]],
groups: []
identities: [:provider, created_by_identity: [:actor]]
]
)
actor = %{
actor
| groups: socket.assigns.actor.groups,
clients: socket.assigns.actor.clients
}
socket =
socket
|> put_flash(:info, "Identity was deleted.")

View File

@@ -69,7 +69,9 @@ defmodule Web.Actors.Users.New do
socket.assigns.subject
) do
socket =
redirect(socket, to: ~p"/#{socket.assigns.account}/actors/users/#{actor}/new_identity")
push_navigate(socket,
to: ~p"/#{socket.assigns.account}/actors/users/#{actor}/new_identity"
)
{:noreply, socket}
else

View File

@@ -102,7 +102,9 @@ defmodule Web.Actors.Users.NewIdentity do
attrs,
socket.assigns.subject
) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/actors/#{socket.assigns.actor}")
socket =
push_navigate(socket, to: ~p"/#{socket.assigns.account}/actors/#{socket.assigns.actor}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -57,7 +57,7 @@ defmodule Web.Clients.Edit do
def handle_event("submit", %{"client" => attrs}, socket) do
with {:ok, client} <-
Clients.update_client(socket.assigns.client, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/clients/#{client}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/clients/#{client}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -124,7 +124,7 @@ defmodule Web.Clients.Show do
navigate={~p"/#{@account}/gateways/#{flow.gateway_id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= flow.gateway.group.name_prefix %>-<%= flow.gateway.name_suffix %>
<%= flow.gateway.group.name %>-<%= flow.gateway.name %>
</.link>
(<%= flow.gateway_remote_ip %>)
</:col>
@@ -162,6 +162,6 @@ defmodule Web.Clients.Show do
def handle_event("delete", _params, socket) do
{:ok, _client} = Clients.delete_client(socket.assigns.client, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/clients")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/clients")}
end
end

View File

@@ -76,7 +76,7 @@ defmodule Web.Flows.Show do
<:label>Gateway</:label>
<:value>
<.link navigate={~p"/#{@account}/gateways/#{@flow.gateway_id}"} class={link_style()}>
<%= @flow.gateway.group.name_prefix %>-<%= @flow.gateway.name_suffix %>
<%= @flow.gateway.group.name %>-<%= @flow.gateway.name %>
</.link>
<div>
Remote IP: <%= @flow.gateway_remote_ip %>

View File

@@ -32,18 +32,18 @@ defmodule Web.Gateways.Show do
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}"}>
<%= @gateway.group.name_prefix %>
<%= @gateway.group.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@gateway.group}?#gateways"}>
Gateways
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/gateways/#{@gateway}"}>
<%= @gateway.name_suffix %>
<%= @gateway.name %>
</.breadcrumb>
</.breadcrumbs>
<.section>
<:title>
Gateway: <code><%= @gateway.name_suffix %></code>
Gateway: <code><%= @gateway.name %></code>
</:title>
<:content>
<.vertical_table id="gateway">
@@ -54,13 +54,13 @@ defmodule Web.Gateways.Show do
navigate={~p"/#{@account}/sites/#{@gateway.group}"}
class="font-bold text-blue-600 dark:text-blue-500 hover:underline"
>
<%= @gateway.group.name_prefix %>
<%= @gateway.group.name %>
</.link>
</:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Instance Name</:label>
<:value><%= @gateway.name_suffix %></:value>
<:label>Name</:label>
<:value><%= @gateway.name %></:value>
</.vertical_table_row>
<.vertical_table_row :if={@todos_enabled?}>
<:label>Connectivity</:label>
@@ -203,7 +203,7 @@ defmodule Web.Gateways.Show do
{:ok, _gateway} = Gateways.delete_gateway(socket.assigns.gateway, socket.assigns.subject)
socket =
redirect(socket,
push_navigate(socket,
to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.gateway.group}"
)

View File

@@ -58,7 +58,7 @@ defmodule Web.Groups.Edit do
def handle_event("submit", %{"group" => attrs}, socket) do
with {:ok, group} <-
Actors.update_group(socket.assigns.group, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -143,7 +143,7 @@ defmodule Web.Groups.EditActors do
with {:ok, group} <-
Actors.update_group(socket.assigns.group, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -51,7 +51,9 @@ defmodule Web.Groups.New do
def handle_event("submit", %{"group" => attrs}, socket) do
with {:ok, group} <-
Actors.create_group(attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}/edit_actors")
socket =
push_navigate(socket, to: ~p"/#{socket.assigns.account}/groups/#{group}/edit_actors")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -116,6 +116,6 @@ defmodule Web.Groups.Show do
def handle_event("delete", _params, socket) do
{:ok, _group} = Actors.delete_group(socket.assigns.group, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/groups")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/groups")}
end
end

View File

@@ -69,7 +69,7 @@ defmodule Web.Policies.Edit do
def handle_event("submit", %{"policy" => policy_params}, socket) do
with {:ok, policy} <-
Policies.update_policy(socket.assigns.policy, policy_params, socket.assigns.subject) do
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
else
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}

View File

@@ -53,7 +53,7 @@ defmodule Web.Policies.New do
type="select"
options={
Enum.map(@resources, fn resource ->
group_names = resource.gateway_groups |> Enum.map(& &1.name_prefix)
group_names = resource.gateway_groups |> Enum.map(& &1.name)
[
key: "#{resource.name} - #{Enum.join(group_names, ",")}",
@@ -103,7 +103,7 @@ defmodule Web.Policies.New do
{:noreply,
push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{site_id}?#resources")}
else
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/policies/#{policy}")}
end
else
{:error, %Ecto.Changeset{} = changeset} ->

View File

@@ -124,7 +124,7 @@ defmodule Web.Policies.Show do
</:col>
<:col :let={flow} label="GATEWAY (IP)">
<.link navigate={~p"/#{@account}/gateways/#{flow.gateway_id}"} class={link_style()}>
<%= flow.gateway.group.name_prefix %>-<%= flow.gateway.name_suffix %>
<%= flow.gateway.group.name %>-<%= flow.gateway.name %>
</.link>
(<%= flow.gateway_remote_ip %>)
</:col>

View File

@@ -59,7 +59,7 @@ defmodule Web.RelayGroups.Edit do
def handle_event("submit", %{"group" => attrs}, socket) do
with {:ok, group} <-
Relays.update_group(socket.assigns.group, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/relay_groups/#{group}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/relay_groups/#{group}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -4,7 +4,7 @@ defmodule Web.RelayGroups.New do
def mount(_params, _session, socket) do
changeset = Relays.new_group()
{:ok, assign(socket, form: to_form(changeset), group: nil)}
{:ok, assign(socket, form: to_form(changeset))}
end
def render(assigns) do
@@ -14,15 +14,12 @@ defmodule Web.RelayGroups.New do
<.breadcrumb path={~p"/#{@account}/relay_groups/new"}>Add</.breadcrumb>
</.breadcrumbs>
<.section>
<:title :if={is_nil(@group)}>
<:title>
Add a new Relay Instance Group
</:title>
<:title :if={not is_nil(@group)}>
Deploy your Relay Instance
</:title>
<:content>
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.input
@@ -38,71 +35,6 @@ defmodule Web.RelayGroups.New do
Save
</.submit_button>
</.form>
<div :if={not is_nil(@group)}>
<div class="text-xl mb-2">
Select deployment method:
</div>
<.tabs id="deployment-instructions" phx-update="ignore">
<:tab id="docker-instructions" label="Docker">
<p class="pl-4 mb-2">
Copy-paste this command to your server and replace <code>PUBLIC_IP4_ADDR</code>
and <code>PUBLIC_IP6_ADDR</code>
with your public IP addresses:
</p>
<.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(@env) %></.code_block>
</:tab>
<:tab id="systemd-instructions" label="Systemd">
<p class="pl-4 mb-2">
1. Create a systemd unit file with the following content:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-relay.service</.code_block>
<p class="pl-4 mb-2 mt-4">
2. Copy-paste the following content into the file and replace
<code>PUBLIC_IP4_ADDR</code>
and <code>PUBLIC_IP6_ADDR</code>
with your public IP addresses::
</p>
<.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %></.code_block>
<p class="pl-4 mb-2 mt-4">
3. Save by pressing <kbd>Ctrl</kbd>+<kbd>X</kbd>, then <kbd>Y</kbd>, then <kbd>Enter</kbd>.
</p>
<p class="pl-4 mb-2 mt-4">
4. Reload systemd configuration:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl daemon-reload</.code_block>
<p class="pl-4 mb-2 mt-4">
5. Start the service:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl start firezone-relay</.code_block>
<p class="pl-4 mb-2 mt-4">
6. Enable the service to start on boot:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl enable firezone-relay</.code_block>
<p class="pl-4 mb-2 mt-4">
7. Check the status of the service:
</p>
<.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-relay</.code_block>
</:tab>
</.tabs>
<div class="mt-4 animate-pulse text-center">
Waiting for relay connection...
</div>
</div>
</div>
</:content>
</.section>
@@ -122,121 +54,10 @@ defmodule Web.RelayGroups.New do
with {:ok, group} <-
Relays.create_group(attrs, socket.assigns.subject) do
:ok = Relays.subscribe_for_relays_presence_in_group(group)
token = encode_group_token(group)
{:noreply, assign(socket, group: group, env: env(token))}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/relay_groups/#{group}")}
else
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
def handle_info(%Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _account_id}, socket) do
socket =
push_redirect(socket,
to: ~p"/#{socket.assigns.account}/relay_groups/#{socket.assigns.group}"
)
{:noreply, socket}
end
defp version do
vsn =
Application.spec(:domain)
|> Keyword.fetch!(:vsn)
|> List.to_string()
|> Version.parse!()
"#{vsn.major}.#{vsn.minor}"
end
defp env(token) do
api_url_override =
if api_url = Domain.Config.get_env(:web, :api_url_override) do
{"FIREZONE_API_URL", api_url}
end
[
{"FIREZONE_ID", Ecto.UUID.generate()},
{"FIREZONE_TOKEN", token},
{"PUBLIC_IP4_ADDR", "YOU_MUST_SET_THIS_VALUE"},
{"PUBLIC_IP6_ADDR", "YOU_MUST_SET_THIS_VALUE"},
api_url_override,
{"RUST_LOG", "warn"},
{"LOG_FORMAT", "google-cloud"}
]
|> Enum.reject(&is_nil/1)
end
defp docker_command(env) do
[
"docker run -d",
"--restart=unless-stopped",
"--pull=always",
"--health-cmd=\"lsof -i UDP | grep firezone-relay\"",
"--name=firezone-relay",
"--cap-add=NET_ADMIN",
"--sysctl net.ipv4.ip_forward=1",
"--sysctl net.ipv4.conf.all.src_valid_mark=1",
"--sysctl net.ipv6.conf.all.disable_ipv6=0",
"--sysctl net.ipv6.conf.all.forwarding=1",
"--sysctl net.ipv6.conf.default.forwarding=1",
"--device=\"/dev/net/tun:/dev/net/tun\"",
Enum.map(env, fn {key, value} -> "--env #{key}=\"#{value}\"" end),
"--env FIREZONE_HOSTNAME=$(hostname)",
"#{Domain.Config.fetch_env!(:domain, :docker_registry)}/relay:#{version()}"
]
|> List.flatten()
|> Enum.join(" \\\n ")
end
defp systemd_command(env) do
"""
[Unit]
Description=Firezone Relay
After=network.target
[Service]
Type=simple
#{Enum.map_join(env, "\n", fn {key, value} -> "Environment=\"#{key}=#{value}\"" end)}
ExecStartPre=/bin/sh -c ' \\
remote_version=$(curl -Ls \\
-H "Accept: application/vnd.github+json" \\
-H "X-GitHub-Api-Version: 2022-11-28" \\
https://api.github.com/repos/firezone/firezone/releases/latest | grep -oP '"'"'(?<="tag_name": ")[^"]*'"'"'); \\
if [ -e /usr/local/bin/firezone-relay ]; then \\
current_version=$(/usr/local/bin/firezone-relay --version | awk '"'"'{print $NF}'"'"'); \\
else \\
current_version=""; \\
fi; \\
if [ ! "$current_version" = "$remote_version" ]; then \\
arch=$(uname -m); \\
case $arch in \\
aarch64) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm64" ;; \\
armv7l) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm" ;; \\
x86_64) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-x64" ;; \\
*) \\
echo "Unsupported architecture"; \\
exit 1 ;; \\
esac; \\
wget -O /usr/local/bin/firezone-relay $bin_url; \\
chmod +x /usr/local/bin/firezone-relay; \\
fi \\
'
ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-relay
ExecStart=FIREZONE_HOSTNAME=$(hostname) /usr/local/bin/firezone-relay
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
"""
end
defp encode_group_token(group) do
Relays.encode_token!(hd(group.tokens))
end
end

View File

@@ -0,0 +1,249 @@
defmodule Web.RelayGroups.NewToken do
use Web, :live_view
alias Domain.Relays
def mount(%{"id" => id}, _session, socket) do
with {:ok, group} <- Relays.fetch_group_by_id(id, socket.assigns.subject) do
{group, env} =
if connected?(socket) do
{:ok, group} =
Relays.update_group(%{group | tokens: []}, %{tokens: [%{}]}, socket.assigns.subject)
:ok = Relays.subscribe_for_relays_presence_in_group(group)
token = Relays.encode_token!(hd(group.tokens))
{group, env(token)}
else
{group, nil}
end
{:ok, assign(socket, group: group, env: env, connected?: false)}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
end
def render(assigns) do
~H"""
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/relay_groups"}>Relays</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/relay_groups/#{@group}"}>
<%= @group.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/relay_groups/#{@group}/new_token"}>Deploy</.breadcrumb>
</.breadcrumbs>
<.section>
<:title>
Deploy your Relay
</:title>
<:content>
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<div class="text-xl mb-2">
Select deployment method:
</div>
<.tabs :if={@env} id="deployment-instructions" phx-update="ignore">
<:tab id="docker-instructions" label="Docker">
<p class="pl-4 mb-2">
Copy-paste this command to your server and replace <code>PUBLIC_IP4_ADDR</code>
and <code>PUBLIC_IP6_ADDR</code>
with your public IP addresses:
</p>
<.code_block id="code-sample-docker1" class="w-full rounded-b" phx-no-format><%= docker_command(@env) %></.code_block>
<.initial_connection_status
:if={@env}
type="relay"
navigate={~p"/#{@account}/relays/#{@group}"}
connected?={@connected?}
/>
<hr />
<p class="pl-4 mb-2 mt-4 text-xl font-semibold">
Troubleshooting
</p>
<p class="pl-4 mb-2 mt-4">
Check the container status:
</p>
<.code_block id="code-sample-docker2" class="w-full" phx-no-format>docker ps --filter "name=firezone-relay"</.code_block>
<p class="pl-4 mb-2 mt-4">
Check the container logs:
</p>
<.code_block id="code-sample-docker3" class="w-full rounded-b" phx-no-format>docker logs firezone-relay</.code_block>
</:tab>
<:tab id="systemd-instructions" label="Systemd">
<p class="pl-4 mb-2">
1. Create a systemd unit file with the following content:
</p>
<.code_block id="code-sample-systemd1" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-relay.service</.code_block>
<p class="pl-4 mb-2 mt-4">
2. Copy-paste the following content into the file and replace
<code>PUBLIC_IP4_ADDR</code>
and <code>PUBLIC_IP6_ADDR</code>
with your public IP addresses::
</p>
<.code_block id="code-sample-systemd2" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %></.code_block>
<p class="pl-4 mb-2 mt-4">
3. Save by pressing <kbd>Ctrl</kbd>+<kbd>X</kbd>, then <kbd>Y</kbd>, then <kbd>Enter</kbd>.
</p>
<p class="pl-4 mb-2 mt-4">
4. Reload systemd configuration:
</p>
<.code_block id="code-sample-systemd4" class="w-full" phx-no-format>sudo systemctl daemon-reload</.code_block>
<p class="pl-4 mb-2 mt-4">
5. Start the service:
</p>
<.code_block id="code-sample-systemd5" class="w-full" phx-no-format>sudo systemctl start firezone-relay</.code_block>
<p class="pl-4 mb-2 mt-4">
6. Enable the service to start on boot:
</p>
<.code_block id="code-sample-systemd6" class="w-full" phx-no-format>sudo systemctl enable firezone-relay</.code_block>
<.initial_connection_status
:if={@env}
type="relay"
navigate={~p"/#{@account}/sites/#{@group}"}
connected?={@connected?}
/>
<hr />
<p class="pl-4 mb-2 mt-4 text-xl font-semibold">
Troubleshooting
</p>
<p class="pl-4 mb-2 mt-4">
Check the status of the service:
</p>
<.code_block id="code-sample-systemd7" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-relay</.code_block>
<p class="pl-4 mb-2 mt-4">
Check the logs:
</p>
<.code_block id="code-sample-systemd8" class="w-full rounded-b" phx-no-format>sudo journalctl -u firezone-relay.service</.code_block>
</:tab>
</.tabs>
</div>
</:content>
</.section>
"""
end
defp version do
vsn =
Application.spec(:domain)
|> Keyword.fetch!(:vsn)
|> List.to_string()
|> Version.parse!()
"#{vsn.major}.#{vsn.minor}"
end
defp env(token) do
api_url_override =
if api_url = Domain.Config.get_env(:web, :api_url_override) do
{"FIREZONE_API_URL", api_url}
end
[
{"FIREZONE_ID", Ecto.UUID.generate()},
{"FIREZONE_TOKEN", token},
{"PUBLIC_IP4_ADDR", "YOU_MUST_SET_THIS_VALUE"},
{"PUBLIC_IP6_ADDR", "YOU_MUST_SET_THIS_VALUE"},
api_url_override,
{"RUST_LOG", "warn"},
{"LOG_FORMAT", "google-cloud"}
]
|> Enum.reject(&is_nil/1)
end
defp docker_command(env) do
[
"docker run -d",
"--restart=unless-stopped",
"--pull=always",
"--health-cmd=\"lsof -i UDP | grep firezone-relay\"",
"--name=firezone-relay",
"--cap-add=NET_ADMIN",
"--sysctl net.ipv4.ip_forward=1",
"--sysctl net.ipv4.conf.all.src_valid_mark=1",
"--sysctl net.ipv6.conf.all.disable_ipv6=0",
"--sysctl net.ipv6.conf.all.forwarding=1",
"--sysctl net.ipv6.conf.default.forwarding=1",
"--device=\"/dev/net/tun:/dev/net/tun\"",
Enum.map(env, fn {key, value} -> "--env #{key}=\"#{value}\"" end),
"--env FIREZONE_NAME=$(hostname)",
"#{Domain.Config.fetch_env!(:domain, :docker_registry)}/relay:#{version()}"
]
|> List.flatten()
|> Enum.join(" \\\n ")
end
defp systemd_command(env) do
"""
[Unit]
Description=Firezone Relay
After=network.target
[Service]
Type=simple
#{Enum.map_join(env, "\n", fn {key, value} -> "Environment=\"#{key}=#{value}\"" end)}
ExecStartPre=/bin/sh -c ' \\
remote_version=$(curl -Ls \\
-H "Accept: application/vnd.github+json" \\
-H "X-GitHub-Api-Version: 2022-11-28" \\
https://api.github.com/repos/firezone/firezone/releases/latest | grep -oP '"'"'(?<="tag_name": ")[^"]*'"'"'); \\
if [ -e /usr/local/bin/firezone-relay ]; then \\
current_version=$(/usr/local/bin/firezone-relay --version | awk '"'"'{print $NF}'"'"'); \\
else \\
current_version=""; \\
fi; \\
if [ ! "$current_version" = "$remote_version" ]; then \\
arch=$(uname -m); \\
case $arch in \\
aarch64) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm64" ;; \\
armv7l) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-arm" ;; \\
x86_64) \\
bin_url="https://github.com/firezone/firezone/releases/download/latest/relay-x64" ;; \\
*) \\
echo "Unsupported architecture"; \\
exit 1 ;; \\
esac; \\
wget -O /usr/local/bin/firezone-relay $bin_url; \\
chmod +x /usr/local/bin/firezone-relay; \\
fi \\
'
ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-relay
ExecStart=FIREZONE_NAME=$(hostname) /usr/local/bin/firezone-relay
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
"""
end
def handle_info(%Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _group_id}, socket) do
{:noreply, assign(socket, connected?: true)}
end
end

View File

@@ -49,40 +49,46 @@ defmodule Web.RelayGroups.Show do
</:value>
</.vertical_table_row>
</.vertical_table>
<!-- Relays table -->
<div class="grid grid-cols-1 p-4 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="col-span-full mb-4 xl:mb-2">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">
Relay Instances
</h1>
</div>
</div>
<div class="relative overflow-x-auto">
<.table id="relays" rows={@group.relays}>
<:col :let={relay} label="INSTANCE">
<.link
navigate={~p"/#{@account}/relays/#{relay.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<code :if={relay.ipv4} class="block text-xs">
<%= relay.ipv4 %>
</code>
<code :if={relay.ipv6} class="block text-xs">
<%= relay.ipv6 %>
</code>
</.link>
</:col>
<:col :let={relay} label="TOKEN CREATED AT">
<.created_by account={@account} schema={relay.token} />
</:col>
<:col :let={relay} label="STATUS">
<.connection_status schema={relay} />
</:col>
<:empty>
<div class="text-center text-slate-500 p-4">No relay instances to display</div>
</:empty>
</.table>
</div>
</div>
</:content>
</.section>
<.section>
<:title>Relays</:title>
<:action>
<.add_button navigate={~p"/#{@account}/relay_groups/#{@group}/new_token"}>
Deploy
</.add_button>
</:action>
<:content>
<div class="relative overflow-x-auto">
<.table id="relays" rows={@group.relays}>
<:col :let={relay} label="INSTANCE">
<.link
navigate={~p"/#{@account}/relays/#{relay.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<code :if={relay.name} class="block text-xs">
<%= relay.name %>
</code>
<code :if={relay.ipv4} class="block text-xs">
<%= relay.ipv4 %>
</code>
<code :if={relay.ipv6} class="block text-xs">
<%= relay.ipv6 %>
</code>
</.link>
</:col>
<:col :let={relay} label="TOKEN CREATED AT">
<.created_by account={@account} schema={relay.token} />
</:col>
<:col :let={relay} label="STATUS">
<.connection_status schema={relay} />
</:col>
<:empty>
<div class="text-center text-slate-500 p-4">No relay instances to display</div>
</:empty>
</.table>
</div>
</:content>
</.section>
@@ -103,7 +109,9 @@ defmodule Web.RelayGroups.Show do
def handle_info(%Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _account_id}, socket) do
socket =
redirect(socket, to: ~p"/#{socket.assigns.account}/relay_groups/#{socket.assigns.group}")
push_navigate(socket,
to: ~p"/#{socket.assigns.account}/relay_groups/#{socket.assigns.group}"
)
{:noreply, socket}
end
@@ -111,6 +119,6 @@ defmodule Web.RelayGroups.Show do
def handle_event("delete", _params, socket) do
# TODO: make sure tokens are all deleted too!
{:ok, _group} = Relays.delete_group(socket.assigns.group, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/relay_groups")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/relay_groups")}
end
end

View File

@@ -28,14 +28,14 @@ defmodule Web.Relays.Show do
<%= @relay.group.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/relays/#{@relay}"}>
<%= @relay.ipv4 || @relay.ipv6 %>
<%= @relay.name || @relay.ipv4 || @relay.ipv6 %>
</.breadcrumb>
</.breadcrumbs>
<.section>
<:title>
Relay:
<.intersperse_blocks>
Relay: <span :if={@relay.name}><%= @relay.name %></span>
<.intersperse_blocks :if={is_nil(@relay.name)}>
<:separator>,&nbsp;</:separator>
<:item :for={ip <- [@relay.ipv4, @relay.ipv6]} :if={not is_nil(ip)}>
@@ -50,6 +50,10 @@ defmodule Web.Relays.Show do
<:label>Instance Group Name</:label>
<:value><%= @relay.group.name %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Name</:label>
<:value><%= @relay.name %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Remote IPv4</:label>
<:value>
@@ -137,7 +141,7 @@ defmodule Web.Relays.Show do
{:ok, _relay} = Relays.delete_relay(socket.assigns.relay, socket.assigns.subject)
socket =
redirect(socket,
push_navigate(socket,
to: ~p"/#{socket.assigns.account}/relay_groups/#{socket.assigns.relay.group}"
)

View File

@@ -226,7 +226,7 @@ defmodule Web.Resources.Components do
class="font-bold text-blue-600 dark:text-blue-500 hover:underline"
target="_blank"
>
<%= gateway_group.name_prefix %>
<%= gateway_group.name %>
</.link>
</div>
</div>

View File

@@ -57,7 +57,7 @@ defmodule Web.Resources.Index do
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<.badge type="info">
<%= gateway_group.name_prefix %>
<%= gateway_group.name %>
</.badge>
</.link>
</:col>

View File

@@ -172,7 +172,7 @@ defmodule Web.Resources.Show do
navigate={~p"/#{@account}/sites/#{gateway_group}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= gateway_group.name_prefix %>
<%= gateway_group.name %>
</.link>
</:col>
<:empty>
@@ -223,7 +223,7 @@ defmodule Web.Resources.Show do
navigate={~p"/#{@account}/gateways/#{flow.gateway_id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= flow.gateway.group.name_prefix %>-<%= flow.gateway.name_suffix %>
<%= flow.gateway.group.name %>-<%= flow.gateway.name %>
</.link>
(<%= flow.gateway_remote_ip %>)
</:col>

View File

@@ -54,7 +54,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Edit do
with {:ok, provider} <-
Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) do
socket =
redirect(socket,
push_navigate(socket,
to:
~p"/#{socket.assigns.account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
)

View File

@@ -68,7 +68,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.New do
with {:ok, provider} <-
Auth.create_provider(socket.assigns.account, attrs, socket.assigns.subject) do
socket =
redirect(socket,
push_navigate(socket,
to:
~p"/#{socket.assigns.account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
)

View File

@@ -112,7 +112,9 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
def handle_event("delete", _params, socket) do
{:ok, _provider} = Auth.delete_provider(socket.assigns.provider, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
{:noreply,
push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
end
def handle_event("enable", _params, socket) do

View File

@@ -57,7 +57,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Edit do
with {:ok, provider} <-
Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) do
socket =
redirect(socket,
push_navigate(socket,
to:
~p"/#{socket.assigns.account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
)

View File

@@ -68,7 +68,7 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.New do
with {:ok, provider} <-
Auth.create_provider(socket.assigns.account, attrs, socket.assigns.subject) do
socket =
redirect(socket,
push_navigate(socket,
to:
~p"/#{socket.assigns.account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
)

View File

@@ -134,7 +134,9 @@ defmodule Web.Settings.IdentityProviders.OpenIDConnect.Show do
def handle_event("delete", _params, socket) do
{:ok, _provider} = Auth.delete_provider(socket.assigns.provider, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
{:noreply,
push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
end
def handle_event("enable", _params, socket) do

View File

@@ -161,6 +161,8 @@ defmodule Web.Settings.IdentityProviders.SAML.Show do
def handle_event("delete", _params, socket) do
{:ok, _provider} = Auth.delete_provider(socket.assigns.provider, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
{:noreply,
push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
end
end

View File

@@ -93,7 +93,9 @@ defmodule Web.Settings.IdentityProviders.System.Show do
def handle_event("delete", _params, socket) do
{:ok, _provider} = Auth.delete_provider(socket.assigns.provider, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
{:noreply,
push_navigate(socket, to: ~p"/#{socket.assigns.account}/settings/identity_providers")}
end
def handle_event("enable", _params, socket) do

View File

@@ -1,4 +1,4 @@
defmodule Web.Auth.SignIn do
defmodule Web.SignIn do
use Web, {:live_view, layout: {Web.Layouts, :public}}
alias Domain.{Auth, Accounts}

View File

@@ -1,4 +1,4 @@
defmodule Web.Auth.Email do
defmodule Web.SignIn.Email do
use Web, {:live_view, layout: {Web.Layouts, :public}}
def mount(

View File

@@ -16,13 +16,13 @@ defmodule Web.Sites.Edit do
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
<%= @group.name_prefix %>
<%= @group.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@group}/edit"}>Edit</.breadcrumb>
</.breadcrumbs>
<.section>
<:title>Edit Site: <code><%= @group.name_prefix %></code></:title>
<:title>Edit Site: <code><%= @group.name %></code></:title>
<:content>
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<.form for={@form} phx-change={:change} phx-submit={:submit}>
@@ -30,7 +30,7 @@ defmodule Web.Sites.Edit do
<div>
<.input
label="Name Prefix"
field={@form[:name_prefix]}
field={@form[:name]}
placeholder="Name of this Site"
required
/>
@@ -57,7 +57,7 @@ defmodule Web.Sites.Edit do
def handle_event("submit", %{"group" => attrs}, socket) do
with {:ok, group} <-
Gateways.update_group(socket.assigns.group, attrs, socket.assigns.subject) do
socket = redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}")
socket = push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}")
{:noreply, socket}
else
{:error, changeset} ->

View File

@@ -36,7 +36,7 @@ defmodule Web.Sites.Index do
navigate={~p"/#{@account}/sites/#{group}"}
class="font-bold text-blue-600 dark:text-blue-500 hover:underline"
>
<%= group.name_prefix %>
<%= group.name %>
</.link>
</:col>
@@ -92,7 +92,7 @@ defmodule Web.Sites.Index do
navigate={~p"/#{@account}/gateways/#{gateway}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline inline-block"
phx-no-format
><%= gateway.name_suffix %></.link>
><%= gateway.name %></.link>
</:item>
<:tail :let={count}>

View File

@@ -4,7 +4,7 @@ defmodule Web.Sites.New do
def mount(_params, _session, socket) do
changeset = Gateways.new_group()
{:ok, assign(socket, form: to_form(changeset), group: nil)}
{:ok, assign(socket, form: to_form(changeset))}
end
def render(assigns) do
@@ -15,20 +15,17 @@ defmodule Web.Sites.New do
</.breadcrumbs>
<.section>
<:title :if={is_nil(@group)}>
<:title>
Add a new Site
</:title>
<:title :if={not is_nil(@group)}>
Deploy your Gateway
</:title>
<:content>
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-16">
<.form :if={is_nil(@group)} for={@form} phx-change={:change} phx-submit={:submit}>
<.form for={@form} phx-change={:change} phx-submit={:submit}>
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<div>
<.input
label="Name Prefix"
field={@form[:name_prefix]}
field={@form[:name]}
placeholder="Name of this Site"
required
/>
@@ -58,7 +55,7 @@ defmodule Web.Sites.New do
with {:ok, group} <-
Gateways.create_group(attrs, socket.assigns.subject) do
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{group}")}
else
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}

View File

@@ -11,13 +11,13 @@ defmodule Web.Sites.NewToken do
:ok = Gateways.subscribe_for_gateways_presence_in_group(group)
token = encode_group_token(group)
token = Gateways.encode_token!(hd(group.tokens))
{group, env(token)}
else
{group, nil}
end
{:ok, assign(socket, group: group, env: env)}
{:ok, assign(socket, group: group, env: env, connected?: false)}
else
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
end
@@ -28,16 +28,13 @@ defmodule Web.Sites.NewToken do
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
<%= @group.name_prefix %>
<%= @group.name %>
</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@group}/new_token"}>Deploy</.breadcrumb>
</.breadcrumbs>
<.section>
<:title :if={is_nil(@group)}>
Add a new Site
</:title>
<:title :if={not is_nil(@group)}>
<:title>
Deploy your Gateway
</:title>
<:content>
@@ -51,20 +48,45 @@ defmodule Web.Sites.NewToken do
Copy-paste this command to your server:
</p>
<.code_block id="code-sample-docker" class="w-full rounded-b" phx-no-format><%= docker_command(@env) %></.code_block>
<.code_block id="code-sample-docker1" class="w-full" phx-no-format><%= docker_command(@env) %></.code_block>
<.initial_connection_status
:if={@env}
type="gateway"
navigate={~p"/#{@account}/sites/#{@group}"}
connected?={@connected?}
/>
<hr />
<p class="pl-4 mb-2 mt-4 text-xl font-semibold">
Troubleshooting
</p>
<p class="pl-4 mb-2 mt-4">
Check the container status:
</p>
<.code_block id="code-sample-docker2" class="w-full" phx-no-format>docker ps --filter "name=firezone-gateway"</.code_block>
<p class="pl-4 mb-2 mt-4">
Check the container logs:
</p>
<.code_block id="code-sample-docker3" class="w-full rounded-b" phx-no-format>docker logs firezone-gateway</.code_block>
</:tab>
<:tab id="systemd-instructions" label="Systemd">
<p class="pl-4 mb-2">
1. Create a systemd unit file with the following content:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-gateway.service</.code_block>
<.code_block id="code-sample-systemd1" class="w-full" phx-no-format>sudo nano /etc/systemd/system/firezone-gateway.service</.code_block>
<p class="pl-4 mb-2 mt-4">
2. Copy-paste the following content into the file:
</p>
<.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %></.code_block>
<.code_block id="code-sample-systemd2" class="w-full rounded-b" phx-no-format><%= systemd_command(@env) %></.code_block>
<p class="pl-4 mb-2 mt-4">
3. Save by pressing <kbd>Ctrl</kbd>+<kbd>X</kbd>, then <kbd>Y</kbd>, then <kbd>Enter</kbd>.
@@ -74,31 +96,46 @@ defmodule Web.Sites.NewToken do
4. Reload systemd configuration:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl daemon-reload</.code_block>
<.code_block id="code-sample-systemd4" class="w-full" phx-no-format>sudo systemctl daemon-reload</.code_block>
<p class="pl-4 mb-2 mt-4">
5. Start the service:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl start firezone-gateway</.code_block>
<.code_block id="code-sample-systemd5" class="w-full" phx-no-format>sudo systemctl start firezone-gateway</.code_block>
<p class="pl-4 mb-2 mt-4">
6. Enable the service to start on boot:
</p>
<.code_block id="code-sample-systemd" class="w-full" phx-no-format>sudo systemctl enable firezone-gateway</.code_block>
<.code_block id="code-sample-systemd6" class="w-full" phx-no-format>sudo systemctl enable firezone-gateway</.code_block>
<p class="pl-4 mb-2 mt-4">
7. Check the status of the service:
<.initial_connection_status
:if={@env}
type="gateway"
navigate={~p"/#{@account}/sites/#{@group}"}
connected?={@connected?}
/>
<hr />
<p class="pl-4 mb-2 mt-4 text-xl font-semibold">
Troubleshooting
</p>
<.code_block id="code-sample-systemd" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-gateway</.code_block>
<p class="pl-4 mb-2 mt-4">
Check the status of the service:
</p>
<.code_block id="code-sample-systemd7" class="w-full rounded-b" phx-no-format>sudo systemctl status firezone-gateway</.code_block>
<p class="pl-4 mb-2 mt-4">
Check the logs:
</p>
<.code_block id="code-sample-systemd8" class="w-full rounded-b" phx-no-format>sudo journalctl -u firezone-gateway.service</.code_block>
</:tab>
</.tabs>
<div :if={@env} class="mt-4 animate-pulse text-center">
Waiting for gateway connection...
</div>
</div>
</:content>
</.section>
@@ -146,7 +183,7 @@ defmodule Web.Sites.NewToken do
"--sysctl net.ipv6.conf.default.forwarding=1",
"--device=\"/dev/net/tun:/dev/net/tun\"",
Enum.map(env, fn {key, value} -> "--env #{key}=\"#{value}\"" end),
"--env FIREZONE_HOSTNAME=$(hostname)",
"--env FIREZONE_NAME=$(hostname)",
"#{Domain.Config.fetch_env!(:domain, :docker_registry)}/gateway:#{version()}"
]
|> List.flatten()
@@ -190,7 +227,7 @@ defmodule Web.Sites.NewToken do
fi \\
'
ExecStartPre=/usr/bin/chmod +x /usr/local/bin/firezone-gateway
ExecStart=FIREZONE_HOSTNAME=$(hostname) /usr/local/bin/firezone-gateway
ExecStart=FIREZONE_NAME=$(hostname) /usr/local/bin/firezone-gateway
Restart=always
RestartSec=3
@@ -199,14 +236,7 @@ defmodule Web.Sites.NewToken do
"""
end
defp encode_group_token(group) do
Gateways.encode_token!(hd(group.tokens))
end
def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do
socket =
redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}")
{:noreply, socket}
def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _group_id}, socket) do
{:noreply, assign(socket, connected?: true)}
end
end

View File

@@ -11,12 +11,15 @@ defmodule Web.Sites.Show do
created_by_identity: [:actor]
]
),
resources = Enum.map(group.connections, & &1.resource),
resources =
group.connections
|> Enum.reject(&is_nil(&1.resource))
|> Enum.map(& &1.resource),
{:ok, resource_actor_groups_peek} <-
Resources.peek_resource_actor_groups(resources, 3, socket.assigns.subject) do
group = %{
group
| gateways: Enum.sort_by(group.gateways, &{&1.online?, &1.name_suffix}, :desc)
| gateways: Enum.sort_by(group.gateways, &{&1.online?, &1.name}, :desc)
}
:ok = Gateways.subscribe_for_gateways_presence_in_group(group)
@@ -31,13 +34,13 @@ defmodule Web.Sites.Show do
<.breadcrumbs account={@account}>
<.breadcrumb path={~p"/#{@account}/sites"}>Sites</.breadcrumb>
<.breadcrumb path={~p"/#{@account}/sites/#{@group}"}>
<%= @group.name_prefix %>
<%= @group.name %>
</.breadcrumb>
</.breadcrumbs>
<.section>
<:title>
Site: <code><%= @group.name_prefix %></code>
Site: <code><%= @group.name %></code>
</:title>
<:action>
<.edit_button navigate={~p"/#{@account}/sites/#{@group}/edit"}>
@@ -49,7 +52,7 @@ defmodule Web.Sites.Show do
<.vertical_table id="group">
<.vertical_table_row>
<:label>Name</:label>
<:value><%= @group.name_prefix %></:value>
<:value><%= @group.name %></:value>
</.vertical_table_row>
<.vertical_table_row>
<:label>Created</:label>
@@ -76,7 +79,7 @@ defmodule Web.Sites.Show do
navigate={~p"/#{@account}/gateways/#{gateway.id}"}
class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
>
<%= gateway.name_suffix %>
<%= gateway.name %>
</.link>
</:col>
<:col :let={gateway} label="REMOTE IP">
@@ -217,7 +220,7 @@ defmodule Web.Sites.Show do
def handle_info(%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id}, socket) do
socket =
redirect(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}")
push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{socket.assigns.group}")
{:noreply, socket}
end
@@ -225,6 +228,6 @@ defmodule Web.Sites.Show do
def handle_event("delete", _params, socket) do
# TODO: make sure tokens are all deleted too!
{:ok, _group} = Gateways.delete_group(socket.assigns.group, socket.assigns.subject)
{:noreply, redirect(socket, to: ~p"/#{socket.assigns.account}/sites")}
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites")}
end
end

View File

@@ -22,6 +22,14 @@ defmodule Web.Router do
plug :accepts, ["html", "xml"]
end
pipeline :home do
plug :accepts, ["html", "xml"]
plug :fetch_session
plug :protect_from_forgery
plug :fetch_live_flash
plug :put_root_layout, {Web.Layouts, :root}
end
pipeline :ensure_authenticated_admin do
plug :ensure_authenticated
plug :ensure_authenticated_actor_type, :account_admin_user
@@ -33,10 +41,16 @@ defmodule Web.Router do
get "/config.xml", BrowserController, :config
end
scope "/", Web do
pipe_through :home
get "/", HomeController, :home
post "/", HomeController, :redirect_to_sign_in
end
scope "/", Web do
pipe_through :public
get "/", RedirectController, :home
get "/healthz", HealthController, :healthz
end
@@ -61,11 +75,11 @@ defmodule Web.Router do
Web.Sandbox,
{Web.Auth, :redirect_if_user_is_authenticated}
] do
live "/", Auth.SignIn
live "/", SignIn
# Adapter-specific routes
## Email
live "/sign_in/providers/email/:provider_id", Auth.Email
live "/sign_in/providers/email/:provider_id", SignIn.Email
end
scope "/sign_in/providers/:provider_id" do
@@ -137,6 +151,7 @@ defmodule Web.Router do
live "/", Index
live "/new", New
live "/:id/edit", Edit
live "/:id/new_token", NewToken
live "/:id", Show
end

View File

@@ -1,4 +1,4 @@
defmodule Web.Acceptance.Auth.EmailTest do
defmodule Web.Acceptance.SignIn.EmailTest do
use Web.AcceptanceCase, async: true
feature "renders success on invalid email to prevent enumeration attacks", %{session: session} do

View File

@@ -52,10 +52,12 @@ defmodule Web.AuthTest do
describe "signed_in_redirect/4" do
test "redirects regular users to the platform url", %{conn: conn, user_subject: subject} do
redirected_to = conn |> signed_in_redirect(subject, "apple", "foo") |> redirected_to()
assert redirected_to =~ "firezone://handle_client_auth_callback?client_csrf_token=foo"
assert redirected_to =~ "firezone://handle_client_auth_callback"
assert redirected_to =~ "client_csrf_token=foo"
redirected_to = conn |> signed_in_redirect(subject, "android", "foo") |> redirected_to()
assert redirected_to =~ "/handle_client_auth_callback?client_csrf_token=foo"
assert redirected_to =~ "/handle_client_auth_callback?"
assert redirected_to =~ "client_csrf_token=foo"
end
test "redirects regular users to sign in if platform url is missing", %{
@@ -73,10 +75,12 @@ defmodule Web.AuthTest do
test "redirects admin user to the platform url", %{conn: conn, admin_subject: subject} do
redirected_to = conn |> signed_in_redirect(subject, "apple", "foo") |> redirected_to()
assert redirected_to =~ "firezone://handle_client_auth_callback?client_csrf_token=foo"
assert redirected_to =~ "firezone://handle_client_auth_callback?"
assert redirected_to =~ "client_csrf_token=foo"
redirected_to = conn |> signed_in_redirect(subject, "android", "foo") |> redirected_to()
assert redirected_to =~ "/handle_client_auth_callback?client_csrf_token=foo"
assert redirected_to =~ "/handle_client_auth_callback?"
assert redirected_to =~ "client_csrf_token=foo"
end
test "redirects admin user to the post-login path if platform url is missing", %{

View File

@@ -1182,9 +1182,6 @@ defmodule Web.AuthControllerTest do
assert redirected_to(conn) == url(~p"/#{account}")
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
assert %{"fz_recent_account_ids" => fz_recent_account_ids} = conn.cookies
assert :erlang.binary_to_term(fz_recent_account_ids) == []
end
test "redirects to the IdP sign out page", %{conn: conn} do
@@ -1229,7 +1226,7 @@ defmodule Web.AuthControllerTest do
assert_receive %Phoenix.Socket.Broadcast{event: "disconnect", topic: ^live_socket_id}
end
test "removes current account id from list of recent ones", %{conn: conn} do
test "does not remove current account id from list of recent ones", %{conn: conn} do
Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark)
account = Fixtures.Accounts.create_account()
@@ -1270,8 +1267,7 @@ defmodule Web.AuthControllerTest do
assert redirected_to(conn) == url(~p"/#{account}")
assert conn.private.plug_session == %{"preferred_locale" => "en_US"}
assert %{"fz_recent_account_ids" => fz_recent_account_ids} = conn.cookies
assert :erlang.binary_to_term(fz_recent_account_ids) == []
assert %{"fz_recent_account_ids" => ^signed_state} = conn.cookies
end
test "works even if user is already logged out", %{conn: conn} do

View File

@@ -0,0 +1,57 @@
defmodule Web.HomeControllerTest do
use Web.ConnCase, async: true
describe "home/2" do
test "renders the form to find the account sign in page", %{conn: conn} do
conn = get(conn, ~p"/")
html = response(conn, 200)
assert html =~ "Account ID or Slug"
assert html =~ "Go to Sign In page"
end
test "renders recently used account", %{conn: conn} do
accounts = [
Fixtures.Accounts.create_account(),
Fixtures.Accounts.create_account()
]
conn = get(conn, ~p"/")
html = response(conn, 200)
for account <- accounts do
refute html =~ account.name
refute html =~ ~p"/#{account.slug}"
end
account_ids =
accounts
|> Enum.map(& &1.id)
|> :erlang.term_to_binary()
%{resp_cookies: %{"fz_recent_account_ids" => %{value: value}}} =
%{build_conn() | secret_key_base: Web.Endpoint.config(:secret_key_base)}
|> put_resp_cookie("fz_recent_account_ids", account_ids, sign: true, secure: true)
conn =
build_conn()
|> put_req_cookie("fz_recent_account_ids", value)
|> get(~p"/")
html = response(conn, 200)
for account <- accounts do
assert html =~ account.name
assert html =~ ~p"/#{account.slug}"
end
end
end
describe "redirect_to_sign_in/2" do
test "redirects to the sign in page", %{conn: conn} do
id = Ecto.UUID.generate()
conn = post(conn, ~p"/", %{"account_id_or_slug" => id})
assert redirected_to(conn) == ~p"/#{id}"
end
end
end

View File

@@ -197,10 +197,11 @@ defmodule Web.Live.Actors.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}/edit")
assert lv
|> form("form", actor: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/actors/#{actor}"}}}
lv
|> form("form", actor: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/actors/#{actor}")
assert actor = Repo.get_by(Domain.Actors.Actor, id: actor.id) |> Repo.preload(:memberships)
assert actor.name == attrs.name
@@ -375,10 +376,11 @@ defmodule Web.Live.Actors.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}/edit")
assert lv
|> form("form", actor: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/actors/#{actor}"}}}
lv
|> form("form", actor: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/actors/#{actor}")
assert actor = Repo.get_by(Domain.Actors.Actor, id: actor.id) |> Repo.preload(:memberships)
assert actor.name == attrs.name

View File

@@ -46,6 +46,45 @@ defmodule Web.Live.Actors.ShowTest do
assert breadcrumbs =~ actor.name
end
test "renders logs table", %{
conn: conn
} do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
client = Fixtures.Clients.create_client(account: account, actor: actor)
flow =
Fixtures.Flows.create_flow(
account: account,
client: client
)
flow = Repo.preload(flow, [:client, gateway: [:group], policy: [:actor_group, :resource]])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}")
[row] =
lv
|> element("#flows")
|> render()
|> table_to_map()
assert row["authorized at"]
assert row["expires at"]
assert row["policy"] =~ flow.policy.actor_group.name
assert row["policy"] =~ flow.policy.resource.name
assert row["client (ip)"] ==
"#{flow.client.name} (#{client.last_seen_remote_ip})"
assert row["gateway (ip)"] ==
"#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)"
end
describe "users" do
setup do
account = Fixtures.Accounts.create_account()
@@ -59,6 +98,30 @@ defmodule Web.Live.Actors.ShowTest do
}
end
test "renders (you) next to subject actor title", %{
account: account,
actor: actor,
identity: identity,
conn: conn
} do
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}")
assert html =~ "(you)"
other_actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: other_actor)
{:ok, _lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}")
refute html =~ "(you)"
end
test "renders actor details", %{
account: account,
actor: actor,
@@ -73,6 +136,7 @@ defmodule Web.Live.Actors.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/actors/#{actor}")
assert html =~ actor.name
assert html =~ "User"
table =

View File

@@ -144,10 +144,11 @@ defmodule Web.Live.Clients.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/clients/#{client}/edit")
assert lv
|> form("form", client: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/clients/#{client}"}}}
lv
|> form("form", client: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/clients/#{client}")
assert client = Repo.get_by(Domain.Clients.Client, id: client.id)
assert client.name == attrs.name

View File

@@ -144,7 +144,7 @@ defmodule Web.Live.Clients.ShowTest do
assert row["policy"] =~ flow.policy.resource.name
assert row["gateway (ip)"] ==
"#{flow.gateway.group.name_prefix}-#{flow.gateway.name_suffix} (189.172.73.153)"
"#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)"
end
test "allows editing clients", %{
@@ -176,10 +176,11 @@ defmodule Web.Live.Clients.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/clients/#{client}")
assert lv
|> element("button", "Delete Client")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/clients"}}}
lv
|> element("button", "Delete Client")
|> render_click()
assert_redirected(lv, ~p"/#{account}/clients")
assert Repo.get(Domain.Clients.Client, client.id).deleted_at
end

View File

@@ -62,8 +62,8 @@ defmodule Web.Live.Gateways.ShowTest do
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Sites"
assert breadcrumbs =~ gateway.group.name_prefix
assert breadcrumbs =~ gateway.name_suffix
assert breadcrumbs =~ gateway.group.name
assert breadcrumbs =~ gateway.name
end
test "renders gateway details", %{
@@ -83,8 +83,8 @@ defmodule Web.Live.Gateways.ShowTest do
|> render()
|> vertical_table_to_map()
assert table["site"] =~ gateway.group.name_prefix
assert table["instance name"] =~ gateway.name_suffix
assert table["site"] =~ gateway.group.name
assert table["name"] =~ gateway.name
assert table["last seen"]
assert table["last seen remote ip"] =~ to_string(gateway.last_seen_remote_ip)
assert table["status"] =~ "Offline"
@@ -162,10 +162,11 @@ defmodule Web.Live.Gateways.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/gateways/#{gateway}")
assert lv
|> element("button", "Delete Gateway")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/sites/#{gateway.group}"}}}
lv
|> element("button", "Delete Gateway")
|> render_click()
assert_redirected(lv, ~p"/#{account}/sites/#{gateway.group}")
assert Repo.get(Domain.Gateways.Gateway, gateway.id).deleted_at
end

View File

@@ -211,9 +211,11 @@ defmodule Web.Live.Groups.EditActorsTest do
|> element("#actor-#{service_account.id} button", "Add")
|> render_click()
assert lv
|> element("button", "Save")
|> render_click() == {:error, {:redirect, %{to: ~p"/#{account}/groups/#{group}"}}}
lv
|> element("button", "Save")
|> render_click()
assert_redirected(lv, ~p"/#{account}/groups/#{group}")
group = Repo.preload(group, :actors, force: true)
group_actor_ids = Enum.map(group.actors, & &1.id)

View File

@@ -167,10 +167,11 @@ defmodule Web.Live.Groups.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/groups/#{group}/edit")
assert lv
|> form("form", group: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/groups/#{group}"}}}
lv
|> form("form", group: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/groups/#{group}")
assert group = Repo.get_by(Domain.Actors.Group, id: group.id)
assert group.name == attrs.name

View File

@@ -118,14 +118,13 @@ defmodule Web.Live.Groups.NewTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/groups/new")
result =
lv
|> form("form", group: attrs)
|> render_submit()
lv
|> form("form", group: attrs)
|> render_submit()
assert group = Repo.get_by(Domain.Actors.Group, name: attrs.name)
assert result == {:error, {:redirect, %{to: ~p"/#{account}/groups/#{group}/edit_actors"}}}
assert_redirected(lv, ~p"/#{account}/groups/#{group}/edit_actors")
assert group.name == attrs.name
refute group.provider_id

View File

@@ -275,10 +275,11 @@ defmodule Web.Live.Groups.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/groups/#{group}")
assert lv
|> element("button", "Delete Group")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/groups"}}}
lv
|> element("button", "Delete Group")
|> render_click()
assert_redirected(lv, ~p"/#{account}/groups")
assert Repo.get(Domain.Actors.Group, group.id).deleted_at
end

View File

@@ -146,10 +146,11 @@ defmodule Web.Live.Policies.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/policies/#{policy}/edit")
assert lv
|> form("form", policy: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/policies/#{policy}"}}}
lv
|> form("form", policy: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/policies/#{policy}")
assert policy = Repo.get_by(Domain.Policies.Policy, id: policy.id)
assert policy.description == attrs.description

View File

@@ -157,7 +157,7 @@ defmodule Web.Live.Policies.ShowTest do
assert row["client, actor (ip)"] =~ to_string(flow.client_remote_ip)
assert row["gateway (ip)"] =~
"#{flow.gateway.group.name_prefix}-#{flow.gateway.name_suffix} (189.172.73.153)"
"#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)"
end
test "allows deleting policy", %{

View File

@@ -143,10 +143,11 @@ defmodule Web.Live.RelayGroups.EditTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/relay_groups/#{group}/edit")
assert lv
|> form("form", group: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/relay_groups/#{group}"}}}
lv
|> form("form", group: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/relay_groups/#{group}")
assert group = Repo.get_by(Domain.Relays.Group, id: group.id)
assert group.name == attrs.name

View File

@@ -113,22 +113,11 @@ defmodule Web.Live.RelayGroups.NewTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/relay_groups/new")
html =
lv
|> form("form", group: attrs)
|> render_submit()
lv
|> form("form", group: attrs)
|> render_submit()
assert html =~ "Select deployment method"
assert html =~ "FIREZONE_TOKEN="
assert html =~ "docker run"
assert html =~ "Waiting for relay connection..."
token = Regex.run(~r/FIREZONE_TOKEN=&quot;([^ ]+)&quot;/, html) |> List.last()
assert {:ok, _token} = Domain.Relays.authorize_relay(token)
group = Repo.get_by(Domain.Relays.Group, name: attrs.name) |> Repo.preload(:tokens)
relay = Fixtures.Relays.create_relay(account: account, group: group)
Domain.Relays.connect_relay(relay, "foo")
group = Repo.get_by(Domain.Relays.Group, name: attrs.name)
assert assert_redirect(lv, ~p"/#{account}/relay_groups/#{group}")
end

View File

@@ -0,0 +1,49 @@
defmodule Web.Live.RelayGroups.NewTokenTest do
use Web.ConnCase, async: true
setup do
account = Fixtures.Accounts.create_account()
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
group = Fixtures.Relays.create_group(account: account)
%{
account: account,
actor: actor,
identity: identity,
group: group
}
end
test "creates a new group on valid attrs and redirects when relay is connected", %{
account: account,
identity: identity,
group: group,
conn: conn
} do
{:ok, lv, html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/relay_groups/#{group}/new_token")
assert html =~ "Select deployment method"
assert html =~ "FIREZONE_TOKEN="
assert html =~ "PUBLIC_IP4_ADDR="
assert html =~ "PUBLIC_IP6_ADDR="
assert html =~ "docker run"
assert html =~ "Waiting for connection..."
assert Regex.run(~r/FIREZONE_ID=([^ ]+)/, html) |> List.last()
token = Regex.run(~r/FIREZONE_TOKEN=([^ ]+)/, html) |> List.last() |> String.trim("&quot;")
:ok = Domain.Relays.subscribe_for_relays_presence_in_group(group)
relay = Fixtures.Relays.create_relay(account: account, group: group)
assert {:ok, _token} = Domain.Relays.authorize_relay(token)
Domain.Relays.connect_relay(relay, "foo")
assert_receive %Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _group_id}
assert element(lv, "#deployment-instructions")
|> render() =~ "Connected, click to continue"
end
end

View File

@@ -164,10 +164,11 @@ defmodule Web.Live.RelayGroups.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/relay_groups/#{group}")
assert lv
|> element("button", "Delete")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/relay_groups"}}}
lv
|> element("button", "Delete")
|> render_click()
assert_redirected(lv, ~p"/#{account}/relay_groups")
assert Repo.get(Domain.Relays.Group, group.id).deleted_at
end

View File

@@ -126,10 +126,11 @@ defmodule Web.Live.Relays.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/relays/#{relay}")
assert lv
|> element("button", "Delete Relay")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/relay_groups/#{relay.group}"}}}
lv
|> element("button", "Delete Relay")
|> render_click()
assert_redirected(lv, ~p"/#{account}/relay_groups/#{relay.group}")
assert Repo.get(Domain.Relays.Relay, relay.id).deleted_at
end

View File

@@ -77,7 +77,7 @@ defmodule Web.Live.Resources.IndexTest do
Enum.each(resource_rows, fn row ->
assert row["name"] =~ resource.name
assert row["address"] =~ resource.address
assert row["sites"] =~ group.name_prefix
assert row["sites"] =~ group.name
assert row["authorized groups"] == "None, create a Policy to grant access."
end)
end

View File

@@ -197,7 +197,7 @@ defmodule Web.Live.Resources.ShowTest do
|> table_to_map()
for gateway_group <- gateway_groups do
assert gateway_group["name"] =~ group.name_prefix
assert gateway_group["name"] =~ group.name
# TODO: check that status is being rendered
end
end
@@ -234,7 +234,7 @@ defmodule Web.Live.Resources.ShowTest do
assert row["policy"] =~ flow.policy.resource.name
assert row["gateway (ip)"] ==
"#{flow.gateway.group.name_prefix}-#{flow.gateway.name_suffix} (189.172.73.153)"
"#{flow.gateway.group.name}-#{flow.gateway.name} (189.172.73.153)"
assert row["client, actor (ip)"] =~ flow.client.name
assert row["client, actor (ip)"] =~ "owned by #{flow.client.actor.name}"

View File

@@ -99,16 +99,13 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do
}
)
result = render_submit(form)
render_submit(form)
assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name)
assert result ==
{:error,
{:redirect,
%{
to:
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
}}}
assert_redirected(
lv,
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
)
assert provider.name == provider_attrs.name
assert provider.adapter == :google_workspace

View File

@@ -86,16 +86,13 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do
}
)
result = render_submit(form)
render_submit(form)
assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name)
assert result ==
{:error,
{:redirect,
%{
to:
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
}}}
assert_redirected(
lv,
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider}/redirect"
)
assert provider.name == provider_attrs.name
assert provider.adapter == :google_workspace

View File

@@ -215,10 +215,11 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/google_workspace/#{provider}")
assert lv
|> element("button", "Delete Identity Provider")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/settings/identity_providers"}}}
lv
|> element("button", "Delete Identity Provider")
|> render_click()
assert_redirected(lv, ~p"/#{account}/settings/identity_providers")
assert Repo.get(Domain.Auth.Provider, provider.id).deleted_at
end

View File

@@ -94,16 +94,13 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.EditTest do
}
)
result = render_submit(form)
render_submit(form)
assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name)
assert result ==
{:error,
{:redirect,
%{
to:
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
}}}
assert_redirected(
lv,
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
)
assert provider.name == provider_attrs.name
assert provider.adapter == :openid_connect

View File

@@ -88,16 +88,13 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.NewTest do
}
)
result = render_submit(form)
render_submit(form)
assert provider = Repo.get_by(Domain.Auth.Provider, name: provider_attrs.name)
assert result ==
{:error,
{:redirect,
%{
to:
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
}}}
assert_redirected(
lv,
~p"/#{account.id}/settings/identity_providers/openid_connect/#{provider}/redirect"
)
assert provider.name == provider_attrs.name
assert provider.adapter == :openid_connect

View File

@@ -154,10 +154,11 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/openid_connect/#{provider}")
assert lv
|> element("button", "Delete Identity Provider")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/settings/identity_providers"}}}
lv
|> element("button", "Delete Identity Provider")
|> render_click()
assert_redirected(lv, ~p"/#{account}/settings/identity_providers")
assert Repo.get(Domain.Auth.Provider, provider.id).deleted_at
end

View File

@@ -145,10 +145,11 @@ defmodule Web.Live.Settings.IdentityProviders.System.ShowTest do
|> authorize_conn(identity)
|> live(~p"/#{account}/settings/identity_providers/system/#{provider}")
assert lv
|> element("button", "Delete Identity Provider")
|> render_click() ==
{:error, {:redirect, %{to: ~p"/#{account}/settings/identity_providers"}}}
lv
|> element("button", "Delete Identity Provider")
|> render_click()
assert_redirected(lv, ~p"/#{account}/settings/identity_providers")
assert Repo.get(Domain.Auth.Provider, provider.id).deleted_at
end

View File

@@ -1,4 +1,4 @@
defmodule Web.Auth.EmailTest do
defmodule Web.SignIn.EmailTest do
use Web.ConnCase, async: true
setup do

View File

@@ -1,4 +1,4 @@
defmodule Web.Auth.SignInTest do
defmodule Web.SignInTest do
use Web.ConnCase, async: true
test "renders active providers on the page", %{conn: conn} do

View File

@@ -59,7 +59,7 @@ defmodule Web.Live.Sites.EditTest do
assert item = Floki.find(html, "[aria-label='Breadcrumb']")
breadcrumbs = String.trim(Floki.text(item))
assert breadcrumbs =~ "Sites"
assert breadcrumbs =~ group.name_prefix
assert breadcrumbs =~ group.name
assert breadcrumbs =~ "Edit"
end
@@ -77,7 +77,7 @@ defmodule Web.Live.Sites.EditTest do
form = form(lv, "form")
assert find_inputs(form) == [
"group[name_prefix]"
"group[name]"
]
end
@@ -96,14 +96,14 @@ defmodule Web.Live.Sites.EditTest do
lv
|> form("form", group: attrs)
|> validate_change(%{group: %{name_prefix: String.duplicate("a", 256)}}, fn form, _html ->
|> validate_change(%{group: %{name: String.duplicate("a", 256)}}, fn form, _html ->
assert form_validation_errors(form) == %{
"group[name_prefix]" => ["should be at most 64 character(s)"]
"group[name]" => ["should be at most 64 character(s)"]
}
end)
|> validate_change(%{group: %{name_prefix: ""}}, fn form, _html ->
|> validate_change(%{group: %{name: ""}}, fn form, _html ->
assert form_validation_errors(form) == %{
"group[name_prefix]" => ["can't be blank"]
"group[name]" => ["can't be blank"]
}
end)
end
@@ -115,7 +115,7 @@ defmodule Web.Live.Sites.EditTest do
conn: conn
} do
other_group = Fixtures.Gateways.create_group(account: account)
attrs = %{name_prefix: other_group.name_prefix}
attrs = %{name: other_group.name}
{:ok, lv, _html} =
conn
@@ -126,7 +126,7 @@ defmodule Web.Live.Sites.EditTest do
|> form("form", group: attrs)
|> render_submit()
|> form_validation_errors() == %{
"group[name_prefix]" => ["has already been taken"]
"group[name]" => ["has already been taken"]
}
end
@@ -136,19 +136,20 @@ defmodule Web.Live.Sites.EditTest do
group: group,
conn: conn
} do
attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name_prefix])
attrs = Fixtures.Gateways.group_attrs() |> Map.take([:name])
{:ok, lv, _html} =
conn
|> authorize_conn(identity)
|> live(~p"/#{account}/sites/#{group}/edit")
assert lv
|> form("form", group: attrs)
|> render_submit() ==
{:error, {:redirect, %{to: ~p"/#{account}/sites/#{group}"}}}
lv
|> form("form", group: attrs)
|> render_submit()
assert_redirected(lv, ~p"/#{account}/sites/#{group}")
assert group = Repo.get_by(Domain.Gateways.Group, id: group.id)
assert group.name_prefix == attrs.name_prefix
assert group.name == attrs.name
end
end

View File

@@ -76,8 +76,8 @@ defmodule Web.Live.Sites.IndexTest do
|> table_to_map()
assert row == %{
"site" => group.name_prefix,
"gateways" => gateway.name_suffix,
"site" => group.name,
"gateways" => gateway.name,
"resources" => resource.name
}
end

Some files were not shown because too many files have changed in this diff Show More