backend: register server api

This commit is contained in:
Nikos Papadakis 2023-06-23 10:05:10 +03:00
parent 6b14c2d1cc
commit 3c779c6b64
Signed by untrusted user who does not match committer: nikos
GPG key ID: 78871F9905ADFF02
9 changed files with 95 additions and 47 deletions

View file

@ -38,24 +38,33 @@ defmodule Prymn.Servers do
def get_server!(id), do: Repo.get!(Server, id) def get_server!(id), do: Repo.get!(Server, id)
@doc """ @doc """
Creates a server.
## Examples
iex> create_server(%{field: value})
{:ok, %Server{}}
iex> create_server(%{field: bad_value})
{:error, %Ecto.Changeset{}}
Start a new server connection with the app.
""" """
def create_server(attrs \\ %{}) do def create_server(attrs \\ %{}) do
# FIXME: Maybe use a cryptographically secure token (if UUID v4 is not one)? # Create a unique registration token
%Server{connection_token: Ecto.UUID.generate()} %Server{registration_token: :crypto.strong_rand_bytes(16)}
|> Server.changeset(attrs) |> Server.changeset(attrs)
|> Repo.insert() |> Repo.insert()
end end
@doc """
Registers a server using a registration token.
"""
def register_server(token, public_ip) do
# TODO: Validate public ip
case Base.decode64(token) do
{:ok, token} ->
from(s in Server, where: s.registration_token == ^token, select: s)
|> Repo.one()
|> update_server(%{public_ip: public_ip})
:error ->
{:error, "token is not a valid base64 encoded string"}
end
end
@doc """ @doc """
Updates a server. Updates a server.

View file

@ -4,10 +4,13 @@ defmodule Prymn.Servers.Server do
schema "servers" do schema "servers" do
field :name, :string field :name, :string
field :ipv4, :string field :public_ip, :string
field :ipv6, :string
field :provider, Ecto.Enum, values: [:Hetzner, :Custom] field :provider, Ecto.Enum, values: [:Hetzner, :Custom]
field :connection_token, Ecto.UUID, redact: true field :registration_token, :binary, redact: true
field :connection_status, Ecto.Enum,
values: [:awaiting, :connecting, :installing, :connected, :disconnected],
default: :awaiting
timestamps() timestamps()
end end
@ -15,8 +18,8 @@ defmodule Prymn.Servers.Server do
@doc false @doc false
def changeset(server, attrs) do def changeset(server, attrs) do
server server
|> cast(attrs, [:name, :provider]) |> cast(attrs, [:name, :provider, :registration_token])
|> validate_required([:name, :provider, :connection_token]) |> validate_required([:name, :provider])
|> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)") |> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)")
end end
end end

View file

@ -477,7 +477,7 @@ defmodule PrymnWeb.CoreComponents do
class="relative w-min p-0 hover:cursor-pointer" class="relative w-min p-0 hover:cursor-pointer"
phx-click={@row_click && @row_click.(row)} phx-click={@row_click && @row_click.(row)}
> >
<div class="block w-min py-4 pr-6"> <div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" /> <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
<span class="relative hover:cursor-pointer"><%= @row_indicator.(row) %></span> <span class="relative hover:cursor-pointer"><%= @row_indicator.(row) %></span>
</div> </div>

View file

@ -0,0 +1,19 @@
defmodule PrymnWeb.ServerController do
use PrymnWeb, :controller
alias Prymn.Servers
@doc """
Used by clients to request a new server connection to the prymn backend
validating their registration token.
"""
def register(conn, %{"token" => token, "ip" => ip}) do
case Servers.register_server(token, ip) do
{:ok, _server} ->
json(conn, %{"connected" => true})
{:error, error} ->
raise inspect(error)
end
end
end

View file

@ -24,11 +24,13 @@ defmodule PrymnWeb.ServerLive.Index do
end end
@impl true @impl true
def handle_info({:connect, server}, socket) do def handle_info({:connect, %Servers.Server{} = server}, socket) do
# TODO: Connect the new server: socket =
# For custom connections: will need to generate a new prompt for the user to execute if server.provider == :Custom,
# For API connections: the "prompt" will be automatic, but same execution server-side do: push_navigate(socket, to: ~p"/servers/#{server}"),
{:noreply, stream_insert(socket, :servers, server)} else: stream_insert(socket, :servers, server)
{:noreply, socket}
end end
@impl true @impl true

View file

@ -12,15 +12,20 @@
rows={@streams.servers} rows={@streams.servers}
row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end} row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
row_indicator={ row_indicator={
fn {_id, _server} -> fn
~H(<span class="text-indigo-600">Connecting</span>) {_id, %Servers.Server{connection_status: :awaiting}} ->
~H(<span class="text-grey-600">Awaiting connection</span>)
{_id, %Servers.Server{connection_status: :connecting}} ->
~H(<span class="text-purple-600">Connecting</span>)
{_id, %Servers.Server{connection_status: :connected}} ->
~H(<span class="text-green-600">Connected</span>)
end end
} }
indicator_label="Status" indicator_label="Status"
> >
<:col :let={{_id, server}} label="Name"><%= server.name %></:col> <:col :let={{_id, server}} label="Name"><%= server.name %></:col>
<:col :let={{_id, server}} label="IPv4"><%= server.ipv4 || "N/A" %></:col>
<:col :let={{_id, server}} label="IPv6"><%= server.ipv6 || "N/A" %></:col>
<:action :let={{id, server}}> <:action :let={{id, server}}>
<.link <.link
phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")} phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")}

View file

@ -1,18 +1,25 @@
<.header> <.header>
<%= @server.name %> Server <%= @server.name %>
<:subtitle>
<span>IPv4: <%= @server.ipv4 || "Not available" %></span>
<span :if={@server.ipv6}>IPv6: <%= @server.ipv6 %></span>
</:subtitle>
<:actions>
<.button>
New site
</.button>
</:actions>
</.header> </.header>
<div class="mt-10"> <section :if={@server.connection_status == :awaiting} class="my-10">
<h2 class="text-xl font-bold">Sites</h2> <p class="mb-9">
Connect to your server using root credentials and execute the following command:
</p>
<div class="group inline-flex items-center rounded-lg bg-gray-800 p-4 pl-6 text-white">
<code class="flex gap-4">
<span class="select-none text-gray-500">#</span>
<span class="flex-1">
your command here that contains token: <%= Base.encode64(@server.registration_token) %>
</span>
</code>
<button type="button" tabindex="-1">
<.icon
name="hero-document-duplicate-solid"
class="invisible ml-4 animate-bounce text-gray-500 group-hover:visible"
/>
</button>
</div> </div>
</section>
<.back navigate={~p"/servers"}>Back to servers</.back> <.back navigate={~p"/servers"}>Back to servers</.back>

View file

@ -25,10 +25,11 @@ defmodule PrymnWeb.Router do
live "/servers/:id", ServerLive.Show live "/servers/:id", ServerLive.Show
end end
# Other scopes may use custom stacks. scope "/api/v1", PrymnWeb do
# scope "/api", PrymnWeb do pipe_through :api
# pipe_through :api
# end post "/servers/register", ServerController, :register
end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:prymn, :dev_routes) do if Application.compile_env(:prymn, :dev_routes) do

View file

@ -2,14 +2,16 @@ defmodule Prymn.Repo.Migrations.CreateServers do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:servers) do create table("servers") do
add :name, :string add :name, :string
add :ipv4, :string add :public_ip, :string
add :ipv6, :string
add :provider, :string add :provider, :string
add :connection_token, :binary add :connection_status, :string
add :registration_token, :binary
timestamps() timestamps()
end end
create index("servers", [:registration_token], unique: true)
end end
end end