feat(server): host schema, context, auth, status transitions

This commit is contained in:
Carsten 2026-04-21 22:02:24 +02:00
parent bab31b7c4e
commit b141ee7816
5 changed files with 174 additions and 0 deletions

View file

@ -0,0 +1,66 @@
defmodule Server.Hosts do
@moduledoc "Host registration, authentication, status tracking."
alias Server.Repo
alias Server.Schema.Host
@spec create_host(String.t()) :: {:ok, {Host.t(), String.t()}} | {:error, Ecto.Changeset.t()}
def create_host(name) do
token = generate_token()
hash = Bcrypt.hash_pwd_salt(token)
%Host{}
|> Host.create_changeset(%{name: name, token_hash: hash})
|> Repo.insert()
|> case do
{:ok, host} -> {:ok, {host, token}}
{:error, cs} -> {:error, cs}
end
end
@spec authenticate(String.t(), String.t()) ::
{:ok, Host.t()} | {:error, :unknown_host | :invalid_token}
def authenticate(name, token) when is_binary(name) and is_binary(token) do
case Repo.get_by(Host, name: name) do
nil ->
Bcrypt.no_user_verify()
{:error, :unknown_host}
host ->
if Bcrypt.verify_pass(token, host.token_hash) do
{:ok, host}
else
{:error, :invalid_token}
end
end
end
@spec mark_online(Host.t(), String.t() | nil) :: {:ok, Host.t()} | {:error, Ecto.Changeset.t()}
def mark_online(%Host{} = host, agent_version) do
host
|> Host.status_changeset(%{
status: "online",
last_seen_at: DateTime.utc_now(),
agent_version: agent_version
})
|> Repo.update()
end
@spec mark_offline(Host.t()) :: {:ok, Host.t()} | {:error, Ecto.Changeset.t()}
def mark_offline(%Host{} = host) do
host
|> Host.status_changeset(%{status: "offline"})
|> Repo.update()
end
@doc "Mark every host offline — called on server boot to clear stale online flags."
@spec mark_all_offline() :: {integer(), nil}
def mark_all_offline do
import Ecto.Query
Repo.update_all(from(h in Host), set: [status: "offline", updated_at: DateTime.utc_now()])
end
defp generate_token do
:crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
end
end

View file

@ -0,0 +1,32 @@
defmodule Server.Schema.Host do
use Ecto.Schema
import Ecto.Changeset
@statuses ~w(never_connected online offline)
schema "hosts" do
field :name, :string
field :token_hash, :string
field :agent_version, :string
field :proxmox_version, :string
field :zfs_version, :string
field :status, :string, default: "never_connected"
field :last_seen_at, :utc_datetime_usec
timestamps(type: :utc_datetime_usec)
end
def create_changeset(host, attrs) do
host
|> cast(attrs, [:name, :token_hash])
|> validate_required([:name, :token_hash])
|> validate_length(:name, min: 1, max: 100)
|> unique_constraint(:name)
end
def status_changeset(host, attrs) do
host
|> cast(attrs, [:status, :last_seen_at, :agent_version])
|> validate_inclusion(:status, @statuses)
end
end