Skip to content

Commit

Permalink
MVP for issuing credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
zacksiri committed Feb 27, 2024
1 parent 39c0691 commit bd52aea
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 20 deletions.
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ config :polar, PolarWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: 4000],
check_origin: false,
code_reloader: true,
debug_errors: false,
debug_errors: true,
secret_key_base: "TzqTbHuNO4m/845kDoNhFVdt2NYb0Ql8IQudyE594mks0WPM6jgK4DiSDrAZQTsJ",
watchers: [
esbuild: {Esbuild, :install_and_run, [:polar, ~w(--sourcemap=inline --watch)]},
Expand Down
8 changes: 8 additions & 0 deletions lib/polar/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ defmodule Polar.Accounts do
to: Space.Manager,
as: :get_credential

defdelegate change_space_credential(credential),
to: Space.Manager,
as: :change_credential

defdelegate change_space_credential(credential, attrs),
to: Space.Manager,
as: :change_credential

defdelegate create_space_credential(space, user, params),
to: Space.Manager,
as: :create_credential
Expand Down
5 changes: 4 additions & 1 deletion lib/polar/accounts/space/credential.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ defmodule Polar.Accounts.Space.Credential do

def expires_in_range, do: @expires_in_range

def types, do: ["lxd", "incus"]

@doc false
def changeset(credential, attrs) do
expires_in_range_values = Enum.map(@expires_in_range, fn r -> r.value end)
Expand All @@ -47,9 +49,10 @@ defmodule Polar.Accounts.Space.Credential do
|> cast(attrs, [:name, :expires_in, :type])
|> generate_token()
|> validate_inclusion(:expires_in, expires_in_range_values)
|> validate_inclusion(:type, ["lxd", "incus"])
|> validate_inclusion(:type, types())
|> maybe_set_expires_at()
|> validate_required([:token, :type, :name])
|> unique_constraint(:name, name: :space_credentials_space_id_name_index)
end

def scope(:active, queryable) do
Expand Down
4 changes: 4 additions & 0 deletions lib/polar/accounts/space/manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ defmodule Polar.Accounts.Space.Manager do
|> Repo.get_by(token: token)
end

def change_credential(credential_or_changeset, attrs \\ %{}) do
Space.Credential.changeset(credential_or_changeset, attrs)
end

def create_credential(%Accounts.Space{} = space, user, params) do
%Space.Credential{space_id: space.id}
|> Space.Credential.changeset(params)
Expand Down
11 changes: 11 additions & 0 deletions lib/polar_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,17 @@ defmodule PolarWeb.CoreComponents do
"""
end

def input(%{type: "radio"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)

~H"""
<input id={@id} type={@type} name={@name} checked={@checked} value={@value} {@rest} />
"""
end

# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
Expand Down
183 changes: 183 additions & 0 deletions lib/polar_web/live/dashboard/credential/new_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
defmodule PolarWeb.Dashboard.Credential.NewLive do
alias PolarWeb.CoreComponents
use PolarWeb, :live_view

alias Polar.Repo
alias Polar.Accounts
alias Polar.Accounts.Space

def render(assigns) do
~H"""
<div class="space-y-10 divide-y divide-gray-900/10">
<div class="grid grid-cols-1 gap-x-8 gap-y-8 md:grid-cols-3">
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-slate-200">
<%= gettext("Create a credential") %>
</h2>
<p class="mt-1 text-sm leading-6 text-slate-400">
<%= gettext("Credentials allow you to control your simplestreams feed.") %>
</p>
</div>
<.simple_form
for={@credential_form}
id="new-credential-form"
phx-submit="save"
phx-change="validate"
class="bg-white shadow-sm ring-1 ring-slate-900/5 sm:rounded-xl md:col-span-2"
>
<div class="px-4 py-6 sm:p-8">
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-4">
<.input field={@credential_form[:name]} label={gettext("Name")} required />
</div>
<div class="col-span-full">
<div class="flex items-center justify-between">
<h2 class="text-sm font-medium leading-6 text-gray-900">
<%= gettext("Expiry days") %>
</h2>
</div>
<fieldset class="mt-2">
<legend class="sr-only"><%= gettext("Choose expiry policy") %></legend>
<input
type="hidden"
name={@credential_form[:expires_in].name}
value={@credential_form.source.changes[:expires_in]}
/>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-6">
<label
:for={range <- Space.Credential.expires_in_range()}
for={Phoenix.HTML.Form.input_id(:credential, :expires_in, range.label)}
class={[
"flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold sm:flex-1 cursor-pointer focus:outline-none",
"#{if @credential_form.source.changes[:expires_in] == range.value, do: "bg-indigo-600 text-white hover:bg-indigo-500", else: "ring-1 ring-inset ring-slate-300 bg-white text-slate-900 hover:bg-slate-50"}"
]}
>
<.input
type="radio"
id={Phoenix.HTML.Form.input_id(:credential, :expires_in, range.label)}
field={@credential_form[:expires_in]}
value={range.value}
class="sr-only"
/>
<span><%= Phoenix.Naming.humanize(range.label) %></span>
</label>
</div>
</fieldset>
</div>
<div class="col-span-full">
<div class="flex items-center justify-between">
<h2 class="text-sm font-medium leading-6 text-gray-900">
<%= gettext("Type") %>
</h2>
</div>
<fieldset class="mt-2">
<legend class="sr-only"><%= gettext("Choose credential type") %></legend>
<input
type="hidden"
name={@credential_form[:type].name}
value={@credential_form.source.changes[:type]}
/>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-6">
<label
:for={type <- Space.Credential.types()}
for={Phoenix.HTML.Form.input_id(:credential, :type, type)}
class={[
"flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold sm:flex-1 cursor-pointer focus:outline-none",
"#{if @credential_form.source.changes[:type] == type, do: "bg-indigo-600 text-white hover:bg-indigo-500", else: "ring-1 ring-inset ring-slate-300 bg-white text-slate-900 hover:bg-slate-50"}"
]}
>
<.input
id={Phoenix.HTML.Form.input_id(:credential, :type, type)}
type="radio"
field={@credential_form[:type]}
value={type}
class="sr-only"
/>
<span><%= type %></span>
</label>
</div>
</fieldset>
<.error :for={
msg <- Enum.map(@credential_form[:type].errors, &CoreComponents.translate_error/1)
}>
<%= msg %>
</.error>
</div>
</div>
</div>
<:actions>
<div class="flex items-center justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<.link
navigate={~p"/dashboard/spaces/#{@space.id}"}
class="text-sm font-semibold leading-6 text-gray-900"
>
<%= gettext("Cancel") %>
</.link>
<.button
type="submit"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<%= gettext("Save") %>
</.button>
</div>
</:actions>
</.simple_form>
</div>
</div>
"""
end

def mount(%{"space_id" => space_id}, _session, %{assigns: assigns} = socket) do
space = Repo.get_by!(Space, owner_id: assigns.current_user.id, id: space_id)

credential_form =
to_form(Accounts.change_space_credential(%Space.Credential{space_id: space.id}))

socket =
socket
|> assign(:credential_form, credential_form)
|> assign(:space, space)
|> assign(:page_title, gettext("New credential"))
|> assign(:current_path, ~p"/dashboard")

{:ok, socket}
end

def handle_event("validate", %{"credential" => credential_params}, %{assigns: assigns} = socket) do
credential_form =
assigns.credential_form.source
|> Accounts.change_space_credential(credential_params)
|> Map.put(:action, :validate)
|> to_form()

socket =
socket
|> assign(:credential_form, credential_form)

{:noreply, socket}
end

def handle_event("save", %{"credential" => credential_params}, %{assigns: assigns} = socket) do
case Accounts.create_space_credential(assigns.space, assigns.current_user, credential_params) do
{:ok, credential} ->
socket =
socket
|> put_flash(:info, gettext("Credential successfully created!"))
|> push_navigate(
to: ~p"/dashboard/spaces/#{assigns.space.id}/credentials/#{credential.id}"
)

{:noreply, socket}

{:error, %Ecto.Changeset{} = changeset} ->
credential_form = to_form(changeset)

socket =
socket
|> assign(:check_errors, true)
|> assign(:credential_form, credential_form)

{:noreply, socket}
end
end
end
78 changes: 78 additions & 0 deletions lib/polar_web/live/dashboard/credential_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule PolarWeb.Dashboard.CredentialLive do
use PolarWeb, :live_view

alias Polar.Repo
alias Polar.Accounts.Space

@cli_tools %{
"lxd" => "lxc",
"incus" => "incus"
}

def render(assigns) do
~H"""
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
<div class="px-4 py-6 sm:px-6">
<h3 class="text-base font-semibold leading-7 text-gray-900">
<%= gettext("Credential Information") %>
</h3>
<p class="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
<%= gettext("Details about the credential and instructions on how to use it.") %>
</p>
</div>
<div class="border-t border-gray-100">
<dl class="divide-y divide-gray-100">
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900"><%= gettext("Name") %></dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<%= @credential.name %>
</dd>
</div>
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900"><%= gettext("Type") %></dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<%= @credential.type %>
</dd>
</div>
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-900"><%= gettext("Expires At") %></dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
<%= if @credential.expires_at do %>
<%= Calendar.strftime(
@credential.expires_at,
"%d %b %Y"
) %>
<% else %>
<%= gettext("Never Expires") %>
<% end %>
</dd>
</div>
<div class="px-4 py-6 sm:gap-4 sm:px-6">
<div class="px-4 py-6 sm:gap-4 sm:px-6 font-mono bg-slate-800 text-slate-300 rounded-md">
<%= Map.fetch!(@cli_tools, @credential.type) %> remote add opsmaru <%= url(
@socket,
~p"/spaces/#{@credential.token}"
) %>
</div>
</div>
</dl>
</div>
</div>
"""
end

def mount(%{"space_id" => space_id, "id" => id}, _session, %{assigns: assigns} = socket) do
space = Repo.get_by!(Space, owner_id: assigns.current_user.id, id: space_id)
credential = Repo.get_by!(Space.Credential, space_id: space.id, id: id)

socket =
socket
|> assign(:page_title, credential.name)
|> assign(:current_path, ~p"/dashboard")
|> assign(:space, space)
|> assign(:credential, credential)
|> assign(:cli_tools, @cli_tools)

{:ok, socket}
end
end
15 changes: 15 additions & 0 deletions lib/polar_web/live/dashboard/space/data_loader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule PolarWeb.Dashboard.Space.DataLoader do
alias Polar.Repo
alias Polar.Accounts.Space

import Ecto.Query, only: [from: 2]

def load_credentials(%Space{} = space) do
from(sc in Space.Credential,
where:
sc.space_id == ^space.id and
sc.current_state == ^"active"
)
|> Repo.all()
end
end
4 changes: 2 additions & 2 deletions lib/polar_web/live/dashboard/space/new_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ defmodule PolarWeb.Dashboard.Space.NewLive do
{:ok, socket}
end

def handle_event("validate", %{"space" => space_params} = params, %{assigns: assigns} = socket) do
def handle_event("validate", %{"space" => space_params}, %{assigns: assigns} = socket) do
space_form =
%Space{owner_id: assigns.current_user.id}
|> Accounts.change_space(space_params)
Expand All @@ -84,7 +84,7 @@ defmodule PolarWeb.Dashboard.Space.NewLive do
socket =
socket
|> put_flash(:info, gettext("Space successfully created!"))
|> push_navigate(to: ~p"/dashboard")
|> push_navigate(to: ~p"/dashboard/spaces/#{space.id}")

{:noreply, socket}

Expand Down
Loading

0 comments on commit bd52aea

Please sign in to comment.