Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/controllers/api/token_controller.ex
defmodule SyntropyWeb.Api.TokenController do
use SyntropyWeb, :controller
alias Syntropy.Persistence
alias Syntropy.Persistence.ApiToken
alias SyntropyWeb.ApiAuth
@token_bytes 24
@doc "GET /api/tokens — list scoped tokens (admin only; digests, never plaintext)."
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
def index(conn, _params) do
case require_persistence() do
:ok ->
json(conn, %{data: Enum.map(Persistence.list_api_tokens(), &token_json/1)})
{:error, response} ->
send_error(conn, response)
end
end
@doc "POST /api/tokens — create a scoped token. The plaintext appears in this response only."
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, params) do
with :ok <- require_persistence(),
{:ok, label} <- parse_label(params),
{:ok, scope} <- parse_scope(params) do
plaintext = "syg_" <> Base.encode16(:crypto.strong_rand_bytes(@token_bytes), case: :lower)
token_id = "token-" <> Base.encode16(:crypto.strong_rand_bytes(4), case: :lower)
attrs = %{
token_id: token_id,
label: label,
token_hash: ApiAuth.hash_token(plaintext),
scope: scope,
active: true
}
case Persistence.insert_api_token(attrs) do
:ok ->
record =
Enum.find(Persistence.list_api_tokens(), &(&1.token_id == token_id))
conn
|> put_status(:created)
|> json(%{data: %{token: plaintext, record: token_json(record)}})
{:error, _reason} ->
send_error(
conn,
{:internal_server_error, "token_store_failed", "The token could not be stored."}
)
end
else
{:error, response} -> send_error(conn, response)
end
end
@doc "DELETE /api/tokens/:id — deactivate a scoped token."
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete(conn, %{"id" => token_id}) do
with :ok <- require_persistence(),
:ok <- Persistence.deactivate_api_token(token_id) do
json(conn, %{data: %{id: token_id, active: false}})
else
{:error, :not_found} ->
send_error(conn, {:not_found, "not_found", "No token with this id exists."})
{:error, {_status, _code, _message} = response} ->
send_error(conn, response)
{:error, _reason} ->
send_error(
conn,
{:internal_server_error, "token_store_failed", "The token could not be updated."}
)
end
end
defp require_persistence do
if Persistence.enabled?() do
:ok
else
{:error,
{:service_unavailable, "persistence_disabled",
"Scoped API tokens require persistence (SYNTROPY_ENABLE_PERSISTENCE)."}}
end
end
defp parse_label(%{"label" => label}) when is_binary(label) do
case String.trim(label) do
"" -> {:error, {:unprocessable_entity, "invalid_request", "A token label is required."}}
trimmed -> {:ok, trimmed}
end
end
defp parse_label(_params),
do: {:error, {:unprocessable_entity, "invalid_request", "A token label is required."}}
defp parse_scope(%{"scope" => scope}) when is_binary(scope) do
if scope in ApiToken.scopes() do
{:ok, scope}
else
{:error, {:unprocessable_entity, "invalid_request", "Scope must be read, write, or admin."}}
end
end
defp parse_scope(_params), do: {:ok, "read"}
defp token_json(nil), do: nil
defp token_json(token) do
%{
id: token.token_id,
label: token.label,
scope: token.scope,
active: token.active,
hash_preview: "sha256:" <> String.slice(token.token_hash, 0, 12),
inserted_at: token.inserted_at
}
end
defp send_error(conn, {status, code, message}) do
conn
|> put_status(status)
|> json(%{error: %{code: code, message: message}})
end
end