Packages

A Phoenix LiveView library for real-time API route management — discover, toggle, audit, and guard your routes from a dashboard

Current section

Files

Jump to
api_management_console lib api_management_console_v2 accounts.ex
Raw

lib/api_management_console_v2/accounts.ex

defmodule ApiManagementConsoleV2.Accounts do
@moduledoc """
User account management with role-based access control.
Accounts are stored in CubDB alongside policies and audit logs.
Passwords are hashed with bcrypt_elixir.
## Roles
• `:admin` — full access: toggle, hide, reset, manage accounts
• `:viewer` — read-only: view routes and audit log, no mutations
## Default account
On first use, if no accounts exist, a default admin is auto-created:
username `"admin"`, password `"admin123"`.
Override via env vars: `API_CONSOLE_ADMIN_USERNAME` / `API_CONSOLE_ADMIN_PASSWORD`.
## Session
After login, the username and role are stored in Plug session:
`:api_console_user``%{username: "john", role: :admin}`
"""
alias ApiManagementConsoleV2.Accounts.Store
alias ApiManagementConsoleV2.Features
@valid_roles [:admin, :viewer]
@doc "Returns the list of valid roles."
def valid_roles, do: @valid_roles
@doc "Parses a role string into an atom. Returns :viewer for unknown values."
def parse_role("admin"), do: :admin
def parse_role("viewer"), do: :viewer
def parse_role(role) when is_atom(role) and role in @valid_roles, do: role
def parse_role(_), do: :viewer
@doc "Authenticate a user. Returns {:ok, role} or {:error, reason}."
def authenticate(username, password) when is_binary(username) and is_binary(password) do
case Store.get(username) do
nil ->
Bcrypt.no_user_verify()
{:error, :invalid_credentials}
account ->
if Bcrypt.verify_pass(password, account.password_hash) do
{:ok, account.role}
else
{:error, :invalid_credentials}
end
end
end
@doc "Create a new account. Returns :ok or {:error, reason}."
def create(username, password, role \\ :viewer)
def create(username, password, role) when is_binary(username) and is_binary(password) do
if Store.get(username) do
{:error, :already_exists}
else
hash = Bcrypt.hash_pwd_salt(password)
account = %{
username: username,
password_hash: hash,
role: role,
created_at: DateTime.utc_now() |> DateTime.to_iso8601()
}
Store.put(account)
:ok
end
end
@doc "Delete an account. Cannot delete the last admin."
def delete(username) when is_binary(username) do
account = Store.get(username)
cond do
is_nil(account) ->
{:error, :not_found}
account.role == :admin and count_admins() <= 1 ->
{:error, :last_admin}
true ->
Store.delete(username)
:ok
end
end
@doc "List all accounts."
def list, do: Store.all()
@doc "Change a user's role."
def set_role(username, role) when role in [:admin, :viewer] do
case Store.get(username) do
nil -> {:error, :not_found}
account ->
Store.put(%{account | role: role})
:ok
end
end
@doc "Change a user's password."
def change_password(username, new_password) when is_binary(new_password) do
case Store.get(username) do
nil -> {:error, :not_found}
account ->
hash = Bcrypt.hash_pwd_salt(new_password)
Store.put(%{account | password_hash: hash})
:ok
end
end
@doc "Ensure at least one admin exists. Creates default if none."
def ensure_admin_exists do
if count_admins() == 0 do
default_username = System.get_env("API_CONSOLE_ADMIN_USERNAME", "admin")
default_password = System.get_env("API_CONSOLE_ADMIN_PASSWORD", "admin123")
create(default_username, default_password, :admin)
end
end
@doc "Count admin accounts."
def count_admins do
Store.all()
|> Enum.count(&(&1.role == :admin))
end
@doc "Returns true if a new account can be created (within tier limit)."
def can_create? do
case Features.max_admins() do
:unlimited -> true
max -> length(Store.all()) < max
end
end
end