diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 431495a44..bc2da4bac 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -72,7 +72,7 @@ defmodule Domain.Auth do alias Domain.Auth.Identity # This session duration is used when IdP doesn't return the token expiration date, - # or no IdP is used (eg. sign in via email or userpass). + # or no IdP is used (eg. sign in via email, userpass, or temp_account). @default_session_duration_hours [ browser: [ account_admin_user: 10, @@ -143,7 +143,7 @@ defmodule Domain.Auth do This functions allows to fetch singleton providers like `email` or `token`. """ def fetch_active_provider_by_adapter(adapter, %Subject{} = subject, opts \\ []) - when adapter in [:email, :userpass] do + when adapter in [:email, :userpass, :temp_account] do with :ok <- ensure_has_permissions(subject, Authorizer.manage_providers_permission()) do Provider.Query.not_disabled() |> Provider.Query.by_adapter(adapter) @@ -170,7 +170,7 @@ defmodule Domain.Auth do def all_third_party_providers!(%Subject{} = subject) do Provider.Query.not_deleted() |> Provider.Query.by_account_id(subject.account.id) - |> Provider.Query.by_adapter({:not_in, [:email, :userpass]}) + |> Provider.Query.by_adapter({:not_in, [:email, :userpass, :temp_account]}) |> Authorizer.for_subject(Provider, subject) |> Repo.all() end diff --git a/elixir/apps/domain/lib/domain/auth/adapter.ex b/elixir/apps/domain/lib/domain/auth/adapter.ex index 42721a74f..4349b009f 100644 --- a/elixir/apps/domain/lib/domain/auth/adapter.ex +++ b/elixir/apps/domain/lib/domain/auth/adapter.ex @@ -72,7 +72,7 @@ defmodule Domain.Auth.Adapter do A callback invoked during sign-in, should verify the secret and return the identity if it's valid, or an error otherwise. - Used by secret-based providers, eg.: UserPass, Email. + Used by secret-based providers, eg.: UserPass, Email, TempAccount. """ @callback verify_secret(%Identity{}, %Context{}, secret :: term()) :: {:ok, %Identity{}, expires_at :: %DateTime{} | nil} diff --git a/elixir/apps/domain/lib/domain/auth/adapters.ex b/elixir/apps/domain/lib/domain/auth/adapters.ex index 23ff288be..61c29c3df 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters.ex @@ -9,7 +9,8 @@ defmodule Domain.Auth.Adapters do microsoft_entra: Domain.Auth.Adapters.MicrosoftEntra, okta: Domain.Auth.Adapters.Okta, jumpcloud: Domain.Auth.Adapters.JumpCloud, - userpass: Domain.Auth.Adapters.UserPass + userpass: Domain.Auth.Adapters.UserPass, + temp_account: Domain.Auth.Adapters.TempAccount } @adapter_names Map.keys(@adapters) @@ -29,7 +30,7 @@ defmodule Domain.Auth.Adapters do def list_user_provisioned_adapters! do enabled_adapters = Domain.Config.compile_config!(:auth_provider_adapters) - enabled_idp_adapters = enabled_adapters -- ~w[email userpass]a + enabled_idp_adapters = enabled_adapters -- ~w[email userpass temp_account]a Map.take(@adapters, enabled_idp_adapters) end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex new file mode 100644 index 000000000..f235db85b --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account.ex @@ -0,0 +1,119 @@ +defmodule Domain.Auth.Adapters.TempAccount do + @moduledoc """ + This is only being used for Launch HN and will be removed + shortly after. + """ + use Supervisor + alias Domain.Repo + alias Domain.Auth.{Identity, Provider, Adapter, Context} + alias Domain.Auth.Adapters.TempAccount.Password + + @behaviour Adapter + @behaviour Adapter.Local + + def start_link(_init_arg) do + Supervisor.start_link(__MODULE__, nil, name: __MODULE__) + end + + @impl true + def init(_init_arg) do + children = [] + + Supervisor.init(children, strategy: :one_for_one) + end + + @impl true + def capabilities do + [ + provisioners: [:manual], + default_provisioner: :manual, + parent_adapter: nil + ] + end + + @impl true + def identity_changeset(%Provider{} = _provider, %Ecto.Changeset{} = changeset) do + changeset + |> Domain.Repo.Changeset.trim_change(:provider_identifier) + |> validate_password() + end + + defp validate_password(changeset) do + data = Map.get(changeset.data, :provider_virtual_state) || %{} + attrs = Ecto.Changeset.get_change(changeset, :provider_virtual_state) || %{} + + Ecto.embedded_load(Password, data, :json) + |> Password.Changeset.changeset(attrs) + |> case do + %{valid?: false} = nested_changeset -> + {changeset, _original_type} = + Repo.Changeset.inject_embedded_changeset( + changeset, + :provider_virtual_state, + nested_changeset + ) + + changeset + + %{valid?: true} = nested_changeset -> + password_hash = Ecto.Changeset.fetch_change!(nested_changeset, :password_hash) + + {changeset, _original_type} = + changeset + |> Ecto.Changeset.put_change(:provider_state, %{"password_hash" => password_hash}) + |> Repo.Changeset.inject_embedded_changeset(:provider_virtual_state, nested_changeset) + + changeset + end + end + + @impl true + def provider_changeset(%Ecto.Changeset{} = changeset) do + changeset + end + + @impl true + def ensure_provisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def ensure_deprovisioned(%Provider{} = provider) do + {:ok, provider} + end + + @impl true + def sign_out(%Provider{} = _provider, %Identity{} = identity, redirect_url) do + {:ok, identity, redirect_url} + end + + @impl true + def verify_secret(%Identity{} = identity, %Context{} = _context, password) + when is_binary(password) do + Identity.Query.not_disabled() + |> Identity.Query.by_id(identity.id) + |> Repo.fetch_and_update(Identity.Query, + with: fn identity -> + password_hash = identity.provider_state["password_hash"] + + cond do + is_nil(password_hash) -> + :invalid_secret + + not Domain.Crypto.equal?(:argon2, password, password_hash) -> + :invalid_secret + + true -> + Ecto.Changeset.change(identity) + end + end + ) + |> case do + {:ok, identity} -> + {:ok, identity, nil} + + {:error, reason} -> + {:error, reason} + end + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex new file mode 100644 index 000000000..b7ac7879b --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password.ex @@ -0,0 +1,10 @@ +defmodule Domain.Auth.Adapters.TempAccount.Password do + use Domain, :schema + + @primary_key false + embedded_schema do + field :password, :string, virtual: true, redact: true + field :password_hash, :string + field :password_confirmation, :string, virtual: true, redact: true + end +end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex new file mode 100644 index 000000000..47f9b2ef2 --- /dev/null +++ b/elixir/apps/domain/lib/domain/auth/adapters/temp_account/password/changeset.ex @@ -0,0 +1,29 @@ +defmodule Domain.Auth.Adapters.TempAccount.Password.Changeset do + use Domain, :changeset + alias Domain.Auth.Adapters.TempAccount.Password + + @fields ~w[password password_confirmation]a + @min_password_length 12 + @max_password_length 72 + + def changeset(%Password{} = struct, attrs) do + struct + |> cast(attrs, @fields) + |> validate_required(@fields) + |> validate_confirmation(:password, required: true) + |> validate_length(:password, + min: @min_password_length, + max: @max_password_length, + count: :bytes + ) + # We can improve password strength checks later if we decide to run this provider in production. + # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") + # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") + # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") + # |> validate_no_repetitive_characters(:password) + # |> validate_no_sequential_characters(:password) + # |> validate_no_public_context(:password) + |> put_hash(:password, :argon2, to: :password_hash) + |> validate_required([:password_hash]) + end +end diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex index 514638b2a..11dd97288 100644 --- a/elixir/apps/domain/lib/domain/auth/provider.ex +++ b/elixir/apps/domain/lib/domain/auth/provider.ex @@ -5,7 +5,8 @@ defmodule Domain.Auth.Provider do field :name, :string field :adapter, Ecto.Enum, - values: ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud userpass]a + values: + ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud userpass temp_account]a field :provisioner, Ecto.Enum, values: ~w[manual just_in_time custom]a field :adapter_config, :map, redact: true diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 96fee049d..e914b8330 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -100,6 +100,7 @@ defmodule Domain.Config.Definitions do * `openid_connect` is used to authenticate users via OpenID Connect, this is recommended for production use; * `email` is used to authenticate users via sign in tokens sent to the email; * `token` is used to authenticate service accounts using an API token; + * `temp_account` is used to authenticate users with a password, only used for Launch HN; * `userpass` is used to authenticate users with username and password, should be used with extreme care and is not recommended for production use. """, @@ -442,9 +443,11 @@ defmodule Domain.Config.Definitions do okta jumpcloud userpass + temp_account token ]a)}}, - default: ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud token]a + default: + ~w[email openid_connect google_workspace microsoft_entra okta jumpcloud token temp_account]a ) ############################################## @@ -669,6 +672,11 @@ defmodule Domain.Config.Definitions do """ defconfig(:feature_rest_api_enabled, :boolean, default: false) + @doc """ + Boolean flag to turn 'Try Firezone' / temporary accounts functionality on/off + """ + defconfig(:feature_temp_accounts, :boolean, default: false) + ############################################## ## Analytics ############################################## diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index aa041a292..4fa25bd2d 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -285,10 +285,10 @@ defmodule Web.CoreComponents do id={@id} class={[ "p-4 text-sm flash-#{@kind}", - @kind == :success && "text-green-800 bg-green-50", - @kind == :info && "text-blue-800 bg-blue-50", - @kind == :warning && "text-yellow-800 bg-yellow-50", - @kind == :error && "text-red-800 bg-red-50", + @kind == :success && "text-green-800 bg-green-100", + @kind == :info && "text-blue-800 bg-blue-100", + @kind == :warning && "text-yellow-800 bg-yellow-100", + @kind == :error && "text-red-800 bg-red-100", @style != "wide" && "mb-4 rounded" ]} role="alert" @@ -1251,6 +1251,12 @@ defmodule Web.CoreComponents do """ end + def provider_icon(%{adapter: :temp_account} = assigns) do + ~H""" + <.icon name="hero-key" {@rest} /> + """ + end + def provider_icon(assigns), do: ~H"" def feature_name(%{feature: :idp_sync} = assigns) do diff --git a/elixir/apps/web/lib/web/components/layouts/app.html.heex b/elixir/apps/web/lib/web/components/layouts/app.html.heex index 5fd597446..36e61f29c 100644 --- a/elixir/apps/web/lib/web/components/layouts/app.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/app.html.heex @@ -125,4 +125,5 @@ <%= @inner_content %> + <.bottom_banner :if={String.starts_with?(@account.slug, "temp_")} /> diff --git a/elixir/apps/web/lib/web/components/navigation_components.ex b/elixir/apps/web/lib/web/components/navigation_components.ex index fb5b61d6f..6900834eb 100644 --- a/elixir/apps/web/lib/web/components/navigation_components.ex +++ b/elixir/apps/web/lib/web/components/navigation_components.ex @@ -3,6 +3,30 @@ defmodule Web.NavigationComponents do use Web, :verified_routes import Web.CoreComponents + def bottom_banner(assigns) do + ~H""" +
+
+

+ <.icon name="hero-exclamation-triangle" class="w-5 h-5 mx-2" /> + + Reminder: This account is temporary and will be deleted! → + + + + Click here to create a free starter account! + + +

+
+
+ """ + end + attr :subject, :any, required: true def topbar(assigns) do diff --git a/elixir/apps/web/lib/web/controllers/auth_controller.ex b/elixir/apps/web/lib/web/controllers/auth_controller.ex index c351846fb..d791fb7c6 100644 --- a/elixir/apps/web/lib/web/controllers/auth_controller.ex +++ b/elixir/apps/web/lib/web/controllers/auth_controller.ex @@ -22,7 +22,7 @@ defmodule Web.AuthController do action_fallback Web.FallbackController @doc """ - This is a callback for the UserPass provider which checks login and password to authenticate the user. + This is a callback for the UserPass/TempAccount provider which checks login and password to authenticate the user. """ def verify_credentials( conn, @@ -59,6 +59,39 @@ defmodule Web.AuthController do end end + def verify_credentials( + conn, + %{ + "account_id_or_slug" => account_id_or_slug, + "provider_id" => provider_id, + "temp_account" => %{ + "secret" => secret + } + } = params + ) do + redirect_params = Web.Auth.take_sign_in_params(params) + context_type = Web.Auth.fetch_auth_context_type!(redirect_params) + context = Web.Auth.get_auth_context(conn, context_type) + nonce = Web.Auth.fetch_token_nonce!(redirect_params) + + with {:ok, provider} <- + Domain.Auth.fetch_active_provider_by_id(provider_id, preload: :account), + provider_identifier = provider_identifier(provider), + {:ok, identity, encoded_fragment} <- + Domain.Auth.sign_in(provider, provider_identifier, nonce, secret, context) do + Web.Auth.signed_in(conn, provider, identity, context, encoded_fragment, redirect_params) + else + {:error, _reason} -> + conn + |> put_flash(:error, "Invalid password.") + |> redirect(to: ~p"/#{account_id_or_slug}?#{redirect_params}") + end + end + + defp provider_identifier(provider) do + "admin_" <> provider.account.slug <> "@firezonedemo.com" + end + @doc """ This is a callback for the Email provider which sends login link. """ diff --git a/elixir/apps/web/lib/web/live/actors/components.ex b/elixir/apps/web/lib/web/live/actors/components.ex index 4b10f4591..7e990d1e9 100644 --- a/elixir/apps/web/lib/web/live/actors/components.ex +++ b/elixir/apps/web/lib/web/live/actors/components.ex @@ -165,6 +165,17 @@ defmodule Web.Actors.Components do """ end + def provider_form(%{provider: %{adapter: :temp_account}} = assigns) do + ~H""" +
+ No other identities can be created using this provider. The temporary account is intended to be used as a brief trial of Firezone. To create a free starter account click here. +
+ """ + end + def next_step_path(:service_account, account) do ~p"/#{account}/actors/service_accounts/new" end diff --git a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex index 1df6774cd..daaa362c4 100644 --- a/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex +++ b/elixir/apps/web/lib/web/live/settings/identity_providers/components.ex @@ -264,6 +264,7 @@ defmodule Web.Settings.IdentityProviders.Components do def adapter_name(:email), do: "Email" def adapter_name(:userpass), do: "Username & Password" + def adapter_name(:temp_account), do: "Temporary Account" def adapter_name(:google_workspace), do: "Google Workspace" def adapter_name(:microsoft_entra), do: "Microsoft Entra" def adapter_name(:okta), do: "Okta" @@ -271,7 +272,7 @@ defmodule Web.Settings.IdentityProviders.Components do def adapter_name(:openid_connect), do: "OpenID Connect" def view_provider(account, %{adapter: adapter} = provider) - when adapter in [:email, :userpass], + when adapter in [:email, :userpass, :temp_account], do: ~p"/#{account}/settings/identity_providers/system/#{provider}" def view_provider(account, %{adapter: :openid_connect} = provider), diff --git a/elixir/apps/web/lib/web/live/sign_in.ex b/elixir/apps/web/lib/web/live/sign_in.ex index 6768ca059..b26ab5cc8 100644 --- a/elixir/apps/web/lib/web/live/sign_in.ex +++ b/elixir/apps/web/lib/web/live/sign_in.ex @@ -2,7 +2,7 @@ defmodule Web.SignIn do use Web, {:live_view, layout: {Web.Layouts, :public}} alias Domain.{Auth, Accounts} - @root_adapters_whitelist [:email, :userpass, :openid_connect] + @root_adapters_whitelist [:email, :userpass, :openid_connect, :temp_account] def mount(%{"account_id_or_slug" => account_id_or_slug} = params, _session, socket) do with {:ok, account} <- Accounts.fetch_account_by_id_or_slug(account_id_or_slug), @@ -88,6 +88,20 @@ defmodule Web.SignIn do /> + <:item :if={adapter_enabled?(@providers_by_adapter, :temp_account)}> +

+ Sign in with a password +

+ + <.providers_group_form + adapter="temp_account" + provider={List.first(@providers_by_adapter[:temp_account])} + account={@account} + flash={@flash} + params={@params} + /> + + <:item :if={adapter_enabled?(@providers_by_adapter, :email)}>

Sign in with email @@ -194,6 +208,54 @@ defmodule Web.SignIn do """ end + def providers_group_form(%{adapter: "temp_account"} = assigns) do + provider_identifier = Phoenix.Flash.get(assigns.flash, :userpass_provider_identifier) + form = to_form(%{"provider_identifier" => provider_identifier}, as: "temp_account") + + assigns = + assigns + |> Map.put(:temp_account_form, form) + |> Map.put(:enabled?, Domain.Config.global_feature_enabled?(:temp_accounts)) + + ~H""" + <.form + :if={@enabled?} + for={@temp_account_form} + action={~p"/#{@account}/sign_in/providers/#{@provider.id}/verify_credentials"} + class="space-y-4 lg:space-y-6" + id="temp_account_form" + phx-update="ignore" + phx-hook="AttachDisableSubmit" + phx-submit={JS.dispatch("form:disable_and_submit", to: "#temp_account_form")} + > +
+ <.input :for={{key, value} <- @params} type="hidden" name={key} value={value} /> + + <.input + field={@temp_account_form[:secret]} + type="password" + label="Password" + placeholder="••••••••" + required + /> +
+ + <.submit_button class="w-full" style="info" icon="hero-key"> + Sign in + + +
+ + Temporary Accounts have been disabled. + +

+ Click here + to create a free starter account. +

+
+ """ + end + def providers_group_form(%{adapter: "email"} = assigns) do provider_identifier = Phoenix.Flash.get(assigns.flash, :email_provider_identifier) form = to_form(%{"provider_identifier" => provider_identifier}, as: "email") diff --git a/elixir/apps/web/lib/web/live/sites/new_token.ex b/elixir/apps/web/lib/web/live/sites/new_token.ex index 8da774f22..8d1e6b4fc 100644 --- a/elixir/apps/web/lib/web/live/sites/new_token.ex +++ b/elixir/apps/web/lib/web/live/sites/new_token.ex @@ -32,10 +32,17 @@ defmodule Web.Sites.NewToken do end def handle_params(params, uri, socket) do + selected_tab = + if String.starts_with?(socket.assigns.account.slug, "temp_") do + "docker-compose-instructions" + else + "systemd-instructions" + end + {:noreply, assign(socket, uri: uri, - selected_tab: Map.get(params, "method", "systemd-instructions") + selected_tab: Map.get(params, "method", selected_tab) )} end @@ -72,6 +79,36 @@ defmodule Web.Sites.NewToken do <.tabs :if={@env} id="deployment-instructions"> + <:tab + :if={String.starts_with?(@account.slug, "temp_")} + id="docker-compose-instructions" + icon="docker" + label="Docker Compose" + phx_click="tab_selected" + selected={@selected_tab == "docker-compose-instructions"} + > +

+ Copy-paste the code block below to a file titled + docker-compose.yml +
Then run docker compose up -d +

+ + <.code_block + id="code-sample-docker1" + class="w-full text-xs whitespace-pre-line" + phx-no-format + phx-update="ignore" + ><%= docker_compose(@env) %> + +

+ Important: + If you need IPv6 support, you must <.link + href="https://docs.docker.com/config/daemon/ipv6" + class={link_style()} + target="_blank" + >enable IPv6 in the Docker daemon. +

+ <:tab id="systemd-instructions" icon="hero-command-line" @@ -321,6 +358,69 @@ defmodule Web.Sites.NewToken do """ end + defp docker_compose(env) do + env = Enum.into(env, %{}) + + """ + services: + firezone-gateway: + image: "ghcr.io/firezone/gateway:1" + init: true + environment: + # Use a unique ID for each Gateway in your Firezone account. If left blank, + # the Gateway will generate a random ID saved in /var/lib/firezone + # - FIREZONE_ID= + + # REQUIRED. The token shown when deploying a Gateway in the admin portal. + - FIREZONE_TOKEN=#{env["FIREZONE_TOKEN"]} + + # REQUIRED. Firezone URL + - FIREZONE_API_URL=#{env["FIREZONE_API_URL"]} + + # Configure log output. Other options are "trace", "debug", "info", "warn", "error", and "off". + # See https://docs.rs/env_logger/latest/env_logger/ for more information. + - RUST_LOG=str0m=warn,info + + # Enable or disable masquerading. Default enabled. Disabling this can prevent + # the Gateway from reaching other hosts in your subnet or the internet. + - FIREZONE_ENABLE_MASQUERADE=1 + + # Human-friendly name to use for this Gateway in the admin portal. + # $(hostname) is used by default if not set. + # - FIREZONE_NAME= + volumes: + # Persist the FIREZONE_ID. Can be omitted if FIREZONE_ID is set above. + - /var/lib/firezone:/var/lib/firezone + cap_add: + - NET_ADMIN + sysctls: + # Enable IP forwarding + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.all.forwarding=1 + - net.ipv6.conf.default.forwarding=1 + healthcheck: + test: ["CMD", "ip", "link", "|", "grep", "tun-firezone"] + interval: 5s + timeout: 10s + retries: 3 + start_period: 1m + devices: + - /dev/net/tun:/dev/net/tun + networks: + fz-net: + + httpbin: + image: "kong/httpbin" + networks: + fz-net: + + networks: + fz-net: + """ + end + def handle_event("tab_selected", %{"id" => id}, socket) do socket |> assign(selected_tab: id) diff --git a/elixir/apps/web/lib/web/live/temp_accounts/index.ex b/elixir/apps/web/lib/web/live/temp_accounts/index.ex new file mode 100644 index 000000000..091e2daf5 --- /dev/null +++ b/elixir/apps/web/lib/web/live/temp_accounts/index.ex @@ -0,0 +1,211 @@ +defmodule Web.TempAccounts.Index do + use Web, {:live_view, layout: {Web.Layouts, :public}} + alias Domain.{Accounts, Actors, Auth, Config} + + def mount(_params, _session, socket) do + if Config.global_feature_enabled?(:temp_accounts) do + socket = + assign(socket, + page_title: "Try Firezone", + account_info: nil, + creation_error: false + ) + + {:ok, socket} + else + raise(Web.LiveErrors.NotFoundError) + end + end + + def render(assigns) do + ~H""" +
+
+ <.hero_logo text="Welcome to Firezone" /> + +
+
+ <.flash flash={@flash} kind={:error} /> + <.flash flash={@flash} kind={:info} /> + <.welcome :if={is_nil(@account_info) and @creation_error == false} /> + <.account + :if={not is_nil(@account_info)} + account={@account_info.account} + password={@account_info.password} + /> +
+ Something went wrong! +
+
+
+
+
+ """ + end + + def welcome(assigns) do + ~H""" +
+

+ Interested in trying out Firezone? You've come to the right place! +

+ +

+ Take Firezone for a spin with a temporary demo account. +

+
+ <.button class="w-full" phx-click="start">Create Temporary Account +
+ +
+
+
or
+
+
+ +

+ Sign up below with a free starter account to keep your data. +

+

+ + Create Free Starter Account + +

+
+ """ + end + + attr :account, :any + attr :password, :string + + def account(assigns) do + ~H""" +
+
+ Your temporary account has been created! +
+ <.flash kind={:warning}> +

+ Warning! +

+
Please save the following information, it will not be displayed again.
+ +
+
+ <.code_block + id="code-sample-systemd0" + class="w-full text-xs whitespace-pre-line rounded" + phx-no-format + ><%= account_details(@account, @password) %> +
+
+
+ <.link class={button_style("primary") ++ ["py-2"]} navigate={~p"/#{@account}"}> + Sign In + +
+
+ """ + end + + def handle_event("start", _params, socket) do + case register_temp_account() do + {:ok, account_info} -> + socket = + socket + |> assign(:account_info, account_info) + + {:noreply, socket} + + {:error, _} -> + socket = + socket + |> assign(:creation_error, true) + + {:noreply, socket} + end + end + + defp register_temp_account do + account_name = random_string(12) + account_slug = "temp_" <> account_name + admin_email = "admin_#{account_slug}@firezonedemo.com" + admin_password = random_string(16) + + Ecto.Multi.new() + |> Ecto.Multi.run( + :account, + fn _repo, _changes -> + Accounts.create_account(%{ + name: "Temp Account #{account_name}", + slug: account_slug, + metadata: %{stripe: %{billing_email: admin_email}} + }) + end + ) + |> Ecto.Multi.run(:everyone_group, fn _repo, %{account: account} -> + Domain.Actors.create_managed_group(account, %{ + name: "Everyone", + membership_rules: [%{operator: true}] + }) + end) + |> Ecto.Multi.run( + :provider, + fn _repo, %{account: account} -> + Auth.create_provider(account, %{ + name: "Temp Account Password", + adapter: :temp_account, + adapter_config: %{} + }) + end + ) + |> Ecto.Multi.run( + :actor, + fn _repo, %{account: account} -> + Actors.create_actor(account, %{ + type: :account_admin_user, + name: "Admin #{account_slug}" + }) + end + ) + |> Ecto.Multi.run( + :identity, + fn _repo, %{actor: actor, provider: provider} -> + Auth.create_identity(actor, provider, %{ + provider_identifier: admin_email, + provider_virtual_state: %{ + "password" => admin_password, + "password_confirmation" => admin_password + } + }) + end + ) + |> Ecto.Multi.run( + :password, + fn _repo, %{} -> {:ok, admin_password} end + ) + |> Domain.Repo.transaction() + end + + defp random_string(length) do + :crypto.strong_rand_bytes(length) + |> Base.encode32() + |> binary_part(0, length) + |> String.downcase() + end + + defp account_details(account, password) do + """ + Account Name: #{account.name} + + Account Slug: #{account.slug} + + Account URL: #{url(~p"/#{account}")} + + Account Password: #{password} + """ + end +end diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index a27e39ae1..91344631a 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -66,6 +66,12 @@ defmodule Web.Router do live "/", SignUp end + scope "/try", Web do + pipe_through :public + + live "/", TempAccounts.Index + end + scope "/:account_id_or_slug", Web do pipe_through [:public, :account, :redirect_if_user_is_authenticated] @@ -89,7 +95,7 @@ defmodule Web.Router do get "/sign_in/client_auth_error", SignInController, :client_auth_error scope "/sign_in/providers/:provider_id" do - # UserPass + # UserPass / Temp Account post "/verify_credentials", AuthController, :verify_credentials # Email diff --git a/elixir/config/config.exs b/elixir/config/config.exs index b12cbc6d8..5ea7d1a31 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -90,7 +90,8 @@ config :domain, :enabled_features, self_hosted_relays: true, policy_conditions: true, multi_site_resources: true, - rest_api: true + rest_api: true, + temp_accounts: true config :domain, sign_up_whitelisted_domains: [] diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index 208ee2357..c977d7d22 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -64,7 +64,8 @@ if config_env() == :prod do self_hosted_relays: compile_config!(:feature_self_hosted_relays_enabled), policy_conditions: compile_config!(:feature_policy_conditions_enabled), multi_site_resources: compile_config!(:feature_multi_site_resources_enabled), - rest_api: compile_config!(:feature_rest_api_enabled) + rest_api: compile_config!(:feature_rest_api_enabled), + temp_accounts: compile_config!(:feature_temp_accounts) config :domain, sign_up_whitelisted_domains: compile_config!(:sign_up_whitelisted_domains)