backend: register server api
This commit is contained in:
		
							parent
							
								
									6b14c2d1cc
								
							
						
					
					
						commit
						3c779c6b64
					
				
					 9 changed files with 95 additions and 47 deletions
				
			
		| 
						 | 
				
			
			@ -38,24 +38,33 @@ defmodule Prymn.Servers do
 | 
			
		|||
  def get_server!(id), do: Repo.get!(Server, id)
 | 
			
		||||
 | 
			
		||||
  @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
 | 
			
		||||
    # FIXME: Maybe use a cryptographically secure token (if UUID v4 is not one)?
 | 
			
		||||
    %Server{connection_token: Ecto.UUID.generate()}
 | 
			
		||||
    # Create a unique registration token
 | 
			
		||||
    %Server{registration_token: :crypto.strong_rand_bytes(16)}
 | 
			
		||||
    |> Server.changeset(attrs)
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
  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 """
 | 
			
		||||
  Updates a server.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,10 +4,13 @@ defmodule Prymn.Servers.Server do
 | 
			
		|||
 | 
			
		||||
  schema "servers" do
 | 
			
		||||
    field :name, :string
 | 
			
		||||
    field :ipv4, :string
 | 
			
		||||
    field :ipv6, :string
 | 
			
		||||
    field :public_ip, :string
 | 
			
		||||
    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()
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -15,8 +18,8 @@ defmodule Prymn.Servers.Server do
 | 
			
		|||
  @doc false
 | 
			
		||||
  def changeset(server, attrs) do
 | 
			
		||||
    server
 | 
			
		||||
    |> cast(attrs, [:name, :provider])
 | 
			
		||||
    |> validate_required([:name, :provider, :connection_token])
 | 
			
		||||
    |> cast(attrs, [:name, :provider, :registration_token])
 | 
			
		||||
    |> validate_required([:name, :provider])
 | 
			
		||||
    |> validate_inclusion(:provider, [:Custom], message: "Provider not available (yet)")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -477,7 +477,7 @@ defmodule PrymnWeb.CoreComponents do
 | 
			
		|||
              class="relative w-min p-0 hover:cursor-pointer"
 | 
			
		||||
              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="relative hover:cursor-pointer"><%= @row_indicator.(row) %></span>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								backend/lib/prymn_web/controllers/server_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								backend/lib/prymn_web/controllers/server_controller.ex
									
									
									
									
									
										Normal 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
 | 
			
		||||
| 
						 | 
				
			
			@ -24,11 +24,13 @@ defmodule PrymnWeb.ServerLive.Index do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_info({:connect, server}, socket) do
 | 
			
		||||
    # TODO: Connect the new server:
 | 
			
		||||
    #       For custom connections: will need to generate a new prompt for the user to execute
 | 
			
		||||
    #       For API connections: the "prompt" will be automatic, but same execution server-side
 | 
			
		||||
    {:noreply, stream_insert(socket, :servers, server)}
 | 
			
		||||
  def handle_info({:connect, %Servers.Server{} = server}, socket) do
 | 
			
		||||
    socket =
 | 
			
		||||
      if server.provider == :Custom,
 | 
			
		||||
        do: push_navigate(socket, to: ~p"/servers/#{server}"),
 | 
			
		||||
        else: stream_insert(socket, :servers, server)
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,15 +12,20 @@
 | 
			
		|||
  rows={@streams.servers}
 | 
			
		||||
  row_click={fn {_id, server} -> JS.navigate(~p"/servers/#{server}") end}
 | 
			
		||||
  row_indicator={
 | 
			
		||||
    fn {_id, _server} ->
 | 
			
		||||
      ~H(<span class="text-indigo-600">Connecting</span>)
 | 
			
		||||
    fn
 | 
			
		||||
      {_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
 | 
			
		||||
  }
 | 
			
		||||
  indicator_label="Status"
 | 
			
		||||
>
 | 
			
		||||
  <: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}}>
 | 
			
		||||
    <.link
 | 
			
		||||
      phx-click={JS.push("delete", value: %{id: server.id}) |> hide("##{id}")}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,25 @@
 | 
			
		|||
<.header>
 | 
			
		||||
  <%= @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>
 | 
			
		||||
  Server <%= @server.name %>
 | 
			
		||||
</.header>
 | 
			
		||||
 | 
			
		||||
<div class="mt-10">
 | 
			
		||||
  <h2 class="text-xl font-bold">Sites</h2>
 | 
			
		||||
</div>
 | 
			
		||||
<section :if={@server.connection_status == :awaiting} class="my-10">
 | 
			
		||||
  <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>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<.back navigate={~p"/servers"}>Back to servers</.back>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,10 +25,11 @@ defmodule PrymnWeb.Router do
 | 
			
		|||
    live "/servers/:id", ServerLive.Show
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Other scopes may use custom stacks.
 | 
			
		||||
  # scope "/api", PrymnWeb do
 | 
			
		||||
  #   pipe_through :api
 | 
			
		||||
  # end
 | 
			
		||||
  scope "/api/v1", PrymnWeb do
 | 
			
		||||
    pipe_through :api
 | 
			
		||||
 | 
			
		||||
    post "/servers/register", ServerController, :register
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Enable LiveDashboard and Swoosh mailbox preview in development
 | 
			
		||||
  if Application.compile_env(:prymn, :dev_routes) do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,14 +2,16 @@ defmodule Prymn.Repo.Migrations.CreateServers do
 | 
			
		|||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def change do
 | 
			
		||||
    create table(:servers) do
 | 
			
		||||
    create table("servers") do
 | 
			
		||||
      add :name, :string
 | 
			
		||||
      add :ipv4, :string
 | 
			
		||||
      add :ipv6, :string
 | 
			
		||||
      add :public_ip, :string
 | 
			
		||||
      add :provider, :string
 | 
			
		||||
      add :connection_token, :binary
 | 
			
		||||
      add :connection_status, :string
 | 
			
		||||
      add :registration_token, :binary
 | 
			
		||||
 | 
			
		||||
      timestamps()
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    create index("servers", [:registration_token], unique: true)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue