Skip to content

Commit

Permalink
improvements to the shared secrets UI
Browse files Browse the repository at this point in the history
  • Loading branch information
joshk committed Dec 15, 2023
1 parent 959a2fe commit 0f8941d
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 16 deletions.
13 changes: 13 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ window.deploymentPolling = (url) => {
document.querySelectorAll('.date-time').forEach(d => {
d.innerHTML = dates.formatDateTime(d.innerHTML)
})

window.addEventListener('phx:sharedsecret:clipcopy', (event) => {
if ("clipboard" in navigator) {
const text = event.detail.secret;
navigator.clipboard.writeText(text).then(() => {
confirm('Content copied to clipboard');
}, () => {
alert('Failed to copy');
});
} else {
alert("Sorry, your browser does not support clipboard copy.");
}
});
20 changes: 20 additions & 0 deletions lib/nerves_hub/products.ex
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ defmodule NervesHub.Products do
Product.changeset(product, %{})
end

def get_shared_secret_auth(product_id, auth_id) do
SharedSecretAuth
|> join(:inner, [ssa], p in assoc(ssa, :product))
|> where([ssa], ssa.id == ^auth_id)
|> where([_, p], p.id == ^product_id)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
auth -> {:ok, auth}
end
end

def get_shared_secret_auth(key) do
SharedSecretAuth
|> join(:inner, [ssa], p in assoc(ssa, :product))
Expand All @@ -163,6 +175,14 @@ defmodule NervesHub.Products do
|> Repo.insert()
end

def deactivate_shared_secret_auth(product, shared_secret_id) do
{:ok, auth} = get_shared_secret_auth(product.id, shared_secret_id)

auth
|> SharedSecretAuth.deactivate_changeset()
|> Repo.update()
end

def devices_csv(%Product{} = product) do
product = Repo.preload(product, [:org, devices: :device_certificates])
data = Enum.map(product.devices, &device_csv_line(&1, product))
Expand Down
5 changes: 4 additions & 1 deletion lib/nerves_hub/products/product.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ defmodule NervesHub.Products.Product do
has_many(:firmwares, Firmware)
has_many(:jitp, CACertificate.JITP)
has_many(:archives, Archive)
has_many(:shared_secret_auth, SharedSecretAuth)

has_many(:shared_secret_auth, SharedSecretAuth,
preload_order: [desc: :deactivated_at, asc: :id]
)

belongs_to(:org, Org, where: [deleted_at: nil])

Expand Down
5 changes: 5 additions & 0 deletions lib/nerves_hub/products/shared_secret_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ defmodule NervesHub.Products.SharedSecretAuth do
|> unique_constraint(:secret)
end

def deactivate_changeset(%__MODULE__{} = auth) do
change(auth)
|> put_change(:deactivated_at, DateTime.truncate(DateTime.utc_now(), :second))
end

defp generate_token() do
:crypto.strong_rand_bytes(32) |> Base.encode64(padding: false)
end
Expand Down
28 changes: 24 additions & 4 deletions lib/nerves_hub_web/live/product/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule NervesHubWeb.Live.Product.Settings do
def mount(_params, _session, socket) do
socket =
socket
|> assign(:shared_secrets, socket.assigns.product.shared_secret_auth)
|> assign(:shared_auth_enabled, DeviceSocketSharedSecretAuth.enabled?())

{:ok, socket}
Expand All @@ -17,14 +18,33 @@ defmodule NervesHubWeb.Live.Product.Settings do

{:ok, product} = Products.update_product(socket.assigns.product, attrs)

{:noreply, assign(socket, :product, product)}
{:reply, assign(socket, :product, product)}
end

def handle_event("add-shared-secret", _params, socket) do
{:ok, _} = NervesHub.Products.create_shared_secret_auth(socket.assigns.product)
{:ok, _} = Products.create_shared_secret_auth(socket.assigns.product)

{:ok, product} = Products.load_shared_secret_auth(socket.assigns.product)
refreshed = Products.load_shared_secret_auth(socket.assigns.product)

{:noreply, assign(socket, :product, product)}
{:reply, assign(socket, :shared_secrets, refreshed.shared_secret_auth)}
end

def handle_event("copy-shared-secret", %{"value" => shared_secret_id}, socket) do
auth =
Enum.find(socket.assigns.product.shared_secret_auth, fn ssa ->
ssa.id == String.to_integer(shared_secret_id)
end)

{:noreply, push_event(socket, "sharedsecret:clipcopy", %{secret: auth.secret})}
end

def handle_event("deactivate-shared-secret", %{"shared_secret_id" => shared_secret_id}, socket) do
product = socket.assigns.product

{:ok, _} = Products.deactivate_shared_secret_auth(product, shared_secret_id)

refreshed = Products.load_shared_secret_auth(product)

{:reply, assign(socket, :shared_secrets, refreshed.shared_secret_auth)}
end
end
52 changes: 41 additions & 11 deletions lib/nerves_hub_web/live/product/settings.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,71 @@

<div class="border-bottom border-dark mt-2 mb-4"></div>

<div class="container pl-0">
<div class="container pl-0 mb-2">
<div class="row align-items-center">
<div class="col col-4">
<h3>Shared Secrets Settings</h3>
<div class="col col-6">
<h3>Device Shared Secret Authentication</h3>
</div>
<div class="col col-2">
<span class="badge bg-warning">Experimental</span>
</div>
</div>
</div>

<p class="p-small mb-3">
Key / Secret stuff
<p class="p-small">
Shared Secret authentication allows Devices to connect to Nerves Hub using a shared key and secret.<br />
When a Device connects for the first time the Device will be registered with the Product ("Just-in-Time registration").
</p>
<p class="p-small">
This authentication strategy is useful for small deployments of Devices, or when prototyping a new Product.<br />
We highly recommend using Device Certificates for situations where security is paramount.
</p>
<p class="p-small mb-5">
Please refer to the <.link navigate="https://docs.nerves-hub.org/nerves-hub-link/shared-secrets">documentation</.link>
on how to configure this with <.link navigate="https://github.com/nerves-hub/nerves_hub_link">NervesHubLink</.link>.
</p>

<%= if @shared_auth_enabled do %>
<%= if Enum.empty?(@product.shared_secret_auth) do %>
<%= if Enum.empty?(@shared_secrets) do %>
No Shared Secrets added.
<% end %>

<table :if={Enum.any?(@product.shared_secret_auth)} class="table table-sm table-hover">
<table :if={Enum.any?(@shared_secrets)} class="table table-sm table-hover">
<thead>
<tr>
<th>Key</th>
<th>Secret</th>
<th>Created at</th>
<th>Deactivated at</th>
<th></th>
<th></th>
</tr>
</thead>
<tr :for={auth <- @product.shared_secret_auth} class="item">
<tr :for={auth <- @shared_secrets} class="item">
<td>
<div class="mobile-label help-text">Key</div>
<%= auth.key %>
</td>
<td>
<div class="mobile-label help-text">Secret</div>
<%= auth.secret %>
<div class="mobile-label help-text">Created at</div>
<%= Date.to_string(auth.inserted_at) %>
</td>
<td>
<div class="mobile-label help-text">Deactivated at</div>
<%= if auth.deactivated_at, do: Date.to_string(auth.deactivated_at) %>
</td>
<td>
<button class="btn btn-secondary" phx-click="copy-shared-secret" value={auth.id}>
Copy Secret
</button>
</td>
<td>
<button :if={is_nil(auth.deactivated_at)} class="btn btn-secondary" phx-click="deactivate-shared-secret" phx-value-shared_secret_id={auth.id} data-confirm="Are you sure?">
Deactivate
</button>

<button :if={auth.deactivated_at} class="btn btn-secondary" disabled={true}>
Deactivated
</button>
</td>
</tr>
</table>
Expand Down

0 comments on commit 0f8941d

Please sign in to comment.