feat(portal): Add 'temp account' feature for launch HN (#6153)

Why:

* As part of our Launch HN, it was recommended to have a way to allow
people to try Firezone without needing to sign up. This commit adds the
changes need to create temporary accounts that are intended to be
deleted after the Launch HN is complete.

## Screenshots

#### Start Page
<img width="1459" alt="Screenshot 2024-08-02 at 11 00 15 AM"
src="https://github.com/user-attachments/assets/9b4c5dd4-52ee-43dc-8b4f-d3cc6389b698">

#### Temp Account Info Page
<img width="1461" alt="Screenshot 2024-08-02 at 11 00 28 AM"
src="https://github.com/user-attachments/assets/7e96360d-a878-4e63-b3f6-cca29d0bd79f">

#### Temp Account Sign In
<img width="1461" alt="Screenshot 2024-08-02 at 11 00 44 AM"
src="https://github.com/user-attachments/assets/f812e72a-7030-4b35-9ac3-3816a056ef55">

#### Bottom Banner
<img width="1462" alt="Screenshot 2024-08-02 at 11 01 02 AM"
src="https://github.com/user-attachments/assets/b5e9d90f-e888-46f1-9bb6-bcc59fe2c6e6">

#### Temp Account Identity Provider
<img width="1461" alt="Screenshot 2024-08-02 at 11 01 35 AM"
src="https://github.com/user-attachments/assets/79b3d7c4-fe3a-45a6-b4de-56d4f2c70f8e">
This commit is contained in:
Brian Manifold
2024-08-05 08:45:22 -07:00
committed by GitHub
parent 8352255499
commit 023d05ece1
20 changed files with 644 additions and 19 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
##############################################

View File

@@ -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

View File

@@ -125,4 +125,5 @@
</.flash>
<%= @inner_content %>
<.bottom_banner :if={String.starts_with?(@account.slug, "temp_")} />
</main>

View File

@@ -3,6 +3,30 @@ defmodule Web.NavigationComponents do
use Web, :verified_routes
import Web.CoreComponents
def bottom_banner(assigns) do
~H"""
<div
id="bottom-banner"
tabindex="-1"
class="fixed bottom-0 start-0 z-50 flex justify-between w-full p-4 bg-primary-400"
>
<div class="flex items-center mx-auto">
<p class="flex items-center text-md font-normal text-neutral-800">
<.icon name="hero-exclamation-triangle" class="w-5 h-5 mx-2" />
<span class="mr-1">
<b>Reminder:</b> This account is temporary and will be deleted! &rarr;
</span>
<span>
<a href={url(~p"/sign_up")} class={[link_style()]}>
Click here to create a free starter account!
</a>
</span>
</p>
</div>
</div>
"""
end
attr :subject, :any, required: true
def topbar(assigns) do

View File

@@ -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.
"""

View File

@@ -165,6 +165,17 @@ defmodule Web.Actors.Components do
"""
end
def provider_form(%{provider: %{adapter: :temp_account}} = assigns) do
~H"""
<div>
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 <a
class={link_style()}
href={url(~p"/sign_up")}
>click here</a>.
</div>
"""
end
def next_step_path(:service_account, account) do
~p"/#{account}/actors/service_accounts/new"
end

View File

@@ -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),

View File

@@ -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>
<:item :if={adapter_enabled?(@providers_by_adapter, :temp_account)}>
<h2 class="text-lg sm:text-xl leading-tight tracking-tight text-neutral-900">
Sign in with a password
</h2>
<.providers_group_form
adapter="temp_account"
provider={List.first(@providers_by_adapter[:temp_account])}
account={@account}
flash={@flash}
params={@params}
/>
</:item>
<:item :if={adapter_enabled?(@providers_by_adapter, :email)}>
<h2 class="text-lg sm:text-xl leading-tight tracking-tight text-neutral-900">
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")}
>
<div class="bg-white grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
<.input :for={{key, value} <- @params} type="hidden" name={key} value={value} />
<.input
field={@temp_account_form[:secret]}
type="password"
label="Password"
placeholder="••••••••"
required
/>
</div>
<.submit_button class="w-full" style="info" icon="hero-key">
Sign in
</.submit_button>
</.form>
<div :if={not @enabled?} class="text-center border rounded py-4">
<span class="text-xl">
Temporary Accounts have been disabled.
</span>
<p>
<a class={link_style()} href={url(~p"/sign_up")}>Click here</a>
to create a free starter account.
</p>
</div>
"""
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")

View File

@@ -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
</div>
<.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"}
>
<p class="p-6">
Copy-paste the code block below to a file titled
<code class="bg-black text-white px-2">docker-compose.yml</code>
<br /> Then run <code class="bg-black text-white px-2">docker compose up -d</code>
</p>
<.code_block
id="code-sample-docker1"
class="w-full text-xs whitespace-pre-line"
phx-no-format
phx-update="ignore"
><%= docker_compose(@env) %></.code_block>
<p class="p-6 pt-0">
<strong>Important:</strong>
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</.link>.
</p>
</:tab>
<: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=<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=<name-of-gateway>
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)

View File

@@ -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"""
<section>
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto lg:py-0">
<.hero_logo text="Welcome to Firezone" />
<div class="w-full col-span-6 mx-auto bg-white rounded shadow md:mt-0 sm:max-w-lg xl:p-0">
<div class="p-6 space-y-4 lg:space-y-6 sm:p-8">
<.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}
/>
<div :if={@creation_error}>
Something went wrong!
</div>
</div>
</div>
</div>
</section>
"""
end
def welcome(assigns) do
~H"""
<div class="text-center">
<p class="mb-4">
Interested in trying out Firezone? You've come to the right place!
</p>
<p class="mb-4">
Take Firezone for a spin with a temporary demo account.
</p>
<div class="py-1">
<.button class="w-full" phx-click="start">Create Temporary Account</.button>
</div>
<div class="flex items-center my-6">
<div class="w-full h-0.5 bg-neutral-200"></div>
<div class="px-5 text-center text-neutral-500">or</div>
<div class="w-full h-0.5 bg-neutral-200"></div>
</div>
<p class="mb-4">
Sign up below with a free starter account to keep your data.
</p>
<p>
<a
class="inline-block bg-white border rounded px-3 py-2 text-sm w-full"
href={url(~p"/sign_up")}
>
Create Free Starter Account
</a>
</p>
</div>
"""
end
attr :account, :any
attr :password, :string
def account(assigns) do
~H"""
<div class="space-y-6">
<div class="text-center text-neutral-900 text-xl">
Your temporary account has been created!
</div>
<.flash kind={:warning}>
<p class="flex items-center gap-1.5 text-sm font-semibold leading-6">
<span class="hero-exclamation-triangle h-4 w-4"></span> Warning!
</p>
<div>Please save the following information, it will not be displayed again.</div>
</.flash>
<div class="text-center">
<div>
<.code_block
id="code-sample-systemd0"
class="w-full text-xs whitespace-pre-line rounded"
phx-no-format
><%= account_details(@account, @password) %></.code_block>
</div>
</div>
<div class="text-base text-center text-neutral-900">
<.link class={button_style("primary") ++ ["py-2"]} navigate={~p"/#{@account}"}>
Sign In
</.link>
</div>
</div>
"""
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

View File

@@ -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

View File

@@ -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: []

View File

@@ -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)