feat: expand REST API with alerts, agents, org settings, members, integrations, metrics, activity endpoints
This commit is contained in:
parent
685e131cb9
commit
a8339797fa
9 changed files with 776 additions and 0 deletions
53
lib/towerops_web/controllers/api/v1/activity_controller.ex
Normal file
53
lib/towerops_web/controllers/api/v1/activity_controller.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
defmodule ToweropsWeb.Api.V1.ActivityController do
|
||||
@moduledoc "API controller for the organization activity feed."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.ActivityFeed
|
||||
|
||||
def index(conn, params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
opts =
|
||||
[]
|
||||
|> maybe_add(:limit, parse_int(params["limit"], 50))
|
||||
|> maybe_add_types(params["types"])
|
||||
|
||||
items = ActivityFeed.list_org_activity(organization_id, opts)
|
||||
|
||||
json(conn, %{data: items})
|
||||
end
|
||||
|
||||
defp maybe_add(opts, key, value), do: Keyword.put(opts, key, value)
|
||||
|
||||
defp maybe_add_types(opts, nil), do: opts
|
||||
|
||||
defp maybe_add_types(opts, types) when is_binary(types) do
|
||||
type_atoms =
|
||||
types
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.map(&safe_to_atom/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
Keyword.put(opts, :types, type_atoms)
|
||||
end
|
||||
|
||||
defp maybe_add_types(opts, _), do: opts
|
||||
|
||||
defp safe_to_atom(str) do
|
||||
String.to_existing_atom(str)
|
||||
rescue
|
||||
ArgumentError -> nil
|
||||
end
|
||||
|
||||
defp parse_int(nil, default), do: default
|
||||
|
||||
defp parse_int(val, default) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{n, _} -> n
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_int(val, _default) when is_integer(val), do: val
|
||||
end
|
||||
100
lib/towerops_web/controllers/api/v1/agents_controller.ex
Normal file
100
lib/towerops_web/controllers/api/v1/agents_controller.ex
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
defmodule ToweropsWeb.Api.V1.AgentsController do
|
||||
@moduledoc "API controller for agent tokens."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Agents
|
||||
|
||||
def index(conn, _params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
agents =
|
||||
organization_id
|
||||
|> Agents.list_organization_agent_tokens()
|
||||
|> Enum.map(&format_agent/1)
|
||||
|
||||
json(conn, %{data: agents})
|
||||
end
|
||||
|
||||
def create(conn, %{"name" => name}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Agents.create_agent_token(organization_id, name) do
|
||||
{:ok, agent_token, raw_token} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{data: Map.put(format_agent(agent_token), :token, raw_token)})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'name' parameter"})
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
agent = Agents.get_agent_token!(id)
|
||||
|
||||
if agent.organization_id == organization_id do
|
||||
device_count = Agents.count_assigned_devices(id)
|
||||
|
||||
json(conn, %{
|
||||
data:
|
||||
format_agent(agent)
|
||||
|> Map.put(:device_count, device_count)
|
||||
})
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
agent = Agents.get_agent_token!(id)
|
||||
|
||||
if agent.organization_id == organization_id do
|
||||
case Agents.delete_agent_token(id) do
|
||||
{:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "")
|
||||
{:error, _} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not delete agent"})
|
||||
end
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Agent not found"})
|
||||
end
|
||||
|
||||
defp format_agent(agent) do
|
||||
%{
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
enabled: agent.enabled,
|
||||
last_seen_at: agent.last_seen_at,
|
||||
last_ip: agent.last_ip,
|
||||
metadata: agent.metadata,
|
||||
inserted_at: agent.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
114
lib/towerops_web/controllers/api/v1/alerts_controller.ex
Normal file
114
lib/towerops_web/controllers/api/v1/alerts_controller.ex
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
defmodule ToweropsWeb.Api.V1.AlertsController do
|
||||
@moduledoc "API controller for alerts."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Alerts
|
||||
|
||||
def index(conn, params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
filters =
|
||||
params
|
||||
|> Map.take(~w(status limit))
|
||||
|> then(fn f ->
|
||||
case params["device_id"] do
|
||||
nil -> f
|
||||
device_id -> Map.put(f, "device_id", device_id)
|
||||
end
|
||||
end)
|
||||
|
||||
alerts =
|
||||
organization_id
|
||||
|> Alerts.list_organization_alerts(filters)
|
||||
|> Enum.map(&format_alert/1)
|
||||
|
||||
json(conn, %{data: alerts})
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
alert = Alerts.get_alert!(id)
|
||||
device = alert.device
|
||||
|
||||
if device && device.organization_id == organization_id do
|
||||
json(conn, %{data: format_alert(alert)})
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
|
||||
def acknowledge(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
user = conn.assigns[:current_user]
|
||||
|
||||
alert = Alerts.get_alert!(id)
|
||||
device = alert.device
|
||||
|
||||
if device && device.organization_id == organization_id do
|
||||
user_id = if user, do: user.id, else: nil
|
||||
|
||||
case Alerts.acknowledge_alert(alert, user_id) do
|
||||
{:ok, updated} ->
|
||||
json(conn, %{data: format_alert(Towerops.Repo.preload(updated, [:device, :acknowledged_by], force: true))})
|
||||
|
||||
{:error, _changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not acknowledge alert"})
|
||||
end
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
|
||||
def resolve(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
alert = Alerts.get_alert!(id)
|
||||
device = alert.device
|
||||
|
||||
if device && device.organization_id == organization_id do
|
||||
case Alerts.resolve_alert(alert) do
|
||||
{:ok, updated} ->
|
||||
json(conn, %{data: format_alert(Towerops.Repo.preload(updated, [:device, :acknowledged_by], force: true))})
|
||||
|
||||
{:error, _changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not resolve alert"})
|
||||
end
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Alert not found"})
|
||||
end
|
||||
|
||||
defp format_alert(alert) do
|
||||
%{
|
||||
id: alert.id,
|
||||
alert_type: alert.alert_type,
|
||||
message: alert.message,
|
||||
triggered_at: alert.triggered_at,
|
||||
acknowledged_at: alert.acknowledged_at,
|
||||
resolved_at: alert.resolved_at,
|
||||
device_id: alert.device_id,
|
||||
device_name: get_in_loaded(alert, :device, :name),
|
||||
acknowledged_by_email: get_in_loaded(alert, :acknowledged_by, :email),
|
||||
gaiia_impact: alert.gaiia_impact,
|
||||
inserted_at: alert.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
defp get_in_loaded(struct, assoc, field) do
|
||||
case Map.get(struct, assoc) do
|
||||
%Ecto.Association.NotLoaded{} -> nil
|
||||
nil -> nil
|
||||
loaded -> Map.get(loaded, field)
|
||||
end
|
||||
end
|
||||
end
|
||||
129
lib/towerops_web/controllers/api/v1/check_results_controller.ex
Normal file
129
lib/towerops_web/controllers/api/v1/check_results_controller.ex
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
defmodule ToweropsWeb.Api.V1.CheckResultsController do
|
||||
@moduledoc "API controller for device checks, metrics, and interfaces."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Snmp
|
||||
|
||||
def checks(conn, %{"device_id" => device_id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case get_org_device(device_id, organization_id) do
|
||||
{:ok, device} ->
|
||||
checks =
|
||||
Monitoring.list_checks(organization_id, device_id: device.id)
|
||||
|> Enum.map(fn check ->
|
||||
latest = Monitoring.get_latest_check_result(check.id)
|
||||
|
||||
%{
|
||||
id: check.id,
|
||||
name: check.name,
|
||||
check_type: check.check_type,
|
||||
enabled: check.enabled,
|
||||
interval_seconds: check.interval_seconds,
|
||||
latest_status: latest && latest.status,
|
||||
latest_value: latest && latest.value,
|
||||
latest_checked_at: latest && latest.checked_at
|
||||
}
|
||||
end)
|
||||
|
||||
json(conn, %{data: checks})
|
||||
|
||||
{:error, status} ->
|
||||
conn |> put_status(status) |> json(%{error: "Device not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def metrics(conn, %{"device_id" => device_id} = params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case get_org_device(device_id, organization_id) do
|
||||
{:ok, device} ->
|
||||
check_type = params["sensor_type"] || params["check_type"]
|
||||
hours = parse_int(params["hours"], 24)
|
||||
from_time = DateTime.add(DateTime.utc_now(), -hours * 3600, :second)
|
||||
|
||||
checks = Monitoring.list_checks(organization_id, device_id: device.id, check_type: check_type)
|
||||
|
||||
metrics =
|
||||
Enum.map(checks, fn check ->
|
||||
results = Monitoring.get_check_results(check.id, from: from_time)
|
||||
|
||||
%{
|
||||
check_id: check.id,
|
||||
check_name: check.name,
|
||||
check_type: check.check_type,
|
||||
data:
|
||||
Enum.map(results, fn r ->
|
||||
%{timestamp: r.checked_at, value: r.value, status: r.status}
|
||||
end)
|
||||
}
|
||||
end)
|
||||
|
||||
json(conn, %{data: metrics})
|
||||
|
||||
{:error, status} ->
|
||||
conn |> put_status(status) |> json(%{error: "Device not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def interfaces(conn, %{"device_id" => device_id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case get_org_device(device_id, organization_id) do
|
||||
{:ok, device} ->
|
||||
device = Towerops.Repo.preload(device, :snmp_device)
|
||||
|
||||
interfaces =
|
||||
case device.snmp_device do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
snmp_device ->
|
||||
Snmp.list_interfaces(snmp_device.id)
|
||||
|> Enum.map(fn iface ->
|
||||
%{
|
||||
id: iface.id,
|
||||
if_index: iface.if_index,
|
||||
if_name: iface.if_name,
|
||||
if_descr: iface.if_descr,
|
||||
if_alias: iface.if_alias,
|
||||
if_speed: iface.if_speed,
|
||||
if_admin_status: iface.if_admin_status,
|
||||
if_oper_status: iface.if_oper_status,
|
||||
monitored: iface.monitored
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
json(conn, %{data: interfaces})
|
||||
|
||||
{:error, status} ->
|
||||
conn |> put_status(status) |> json(%{error: "Device not found"})
|
||||
end
|
||||
end
|
||||
|
||||
defp get_org_device(device_id, organization_id) do
|
||||
device = Devices.get_device!(device_id)
|
||||
|
||||
if device.organization_id == organization_id do
|
||||
{:ok, device}
|
||||
else
|
||||
{:error, :forbidden}
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError -> {:error, :not_found}
|
||||
end
|
||||
|
||||
defp parse_int(nil, default), do: default
|
||||
|
||||
defp parse_int(val, default) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{n, _} -> n
|
||||
:error -> default
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_int(val, _default) when is_integer(val), do: val
|
||||
end
|
||||
123
lib/towerops_web/controllers/api/v1/integrations_controller.ex
Normal file
123
lib/towerops_web/controllers/api/v1/integrations_controller.ex
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
defmodule ToweropsWeb.Api.V1.IntegrationsController do
|
||||
@moduledoc "API controller for third-party integrations."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Integrations
|
||||
|
||||
def index(conn, _params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
integrations =
|
||||
organization_id
|
||||
|> Integrations.list_integrations()
|
||||
|> Enum.map(&format_integration/1)
|
||||
|
||||
json(conn, %{data: integrations})
|
||||
end
|
||||
|
||||
def create(conn, %{"integration" => attrs}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Integrations.create_integration(organization_id, to_atom_keys(attrs)) do
|
||||
{:ok, integration} ->
|
||||
conn |> put_status(:created) |> json(%{data: format_integration(integration)})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'integration' parameter"})
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Integrations.get_integration_by_id(id) do
|
||||
{:ok, integration} when integration.organization_id == organization_id ->
|
||||
json(conn, %{data: format_integration(integration)})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Integration not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "integration" => attrs}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Integrations.get_integration_by_id(id) do
|
||||
{:ok, integration} when integration.organization_id == organization_id ->
|
||||
case Integrations.update_integration(integration, to_atom_keys(attrs)) do
|
||||
{:ok, updated} -> json(conn, %{data: format_integration(updated)})
|
||||
{:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Integration not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'integration' parameter"})
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Integrations.get_integration_by_id(id) do
|
||||
{:ok, integration} when integration.organization_id == organization_id ->
|
||||
case Integrations.delete_integration(integration) do
|
||||
{:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "")
|
||||
{:error, _} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not delete integration"})
|
||||
end
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Integration not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def test_connection(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Integrations.get_integration_by_id(id) do
|
||||
{:ok, integration} when integration.organization_id == organization_id ->
|
||||
# Simple connectivity test — just verify the integration exists and is configured
|
||||
json(conn, %{data: %{status: "ok", provider: integration.provider}})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Integration not found"})
|
||||
end
|
||||
end
|
||||
|
||||
defp format_integration(integration) do
|
||||
%{
|
||||
id: integration.id,
|
||||
provider: integration.provider,
|
||||
enabled: integration.enabled,
|
||||
sync_interval_minutes: integration.sync_interval_minutes,
|
||||
last_synced_at: integration.last_synced_at,
|
||||
last_sync_status: integration.last_sync_status,
|
||||
inserted_at: integration.inserted_at,
|
||||
updated_at: integration.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
defp to_atom_keys(map) when is_map(map) do
|
||||
Map.new(map, fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
rescue
|
||||
ArgumentError -> map
|
||||
end
|
||||
|
||||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
defmodule ToweropsWeb.Api.V1.InvitationsController do
|
||||
@moduledoc "API controller for organization invitations."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Organizations
|
||||
|
||||
def index(conn, _params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
invitations =
|
||||
organization_id
|
||||
|> Organizations.list_pending_invitations()
|
||||
|> Enum.map(&format_invitation/1)
|
||||
|
||||
json(conn, %{data: invitations})
|
||||
end
|
||||
|
||||
def create(conn, %{"email" => email, "role" => role}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
user = conn.assigns[:current_user]
|
||||
|
||||
attrs = %{
|
||||
organization_id: organization_id,
|
||||
email: email,
|
||||
role: role,
|
||||
invited_by_id: user && user.id,
|
||||
token: Ecto.UUID.generate(),
|
||||
expires_at: DateTime.add(DateTime.utc_now(), 7 * 86_400, :second) |> DateTime.truncate(:second)
|
||||
}
|
||||
|
||||
case Organizations.create_invitation(attrs) do
|
||||
{:ok, invitation} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{data: format_invitation(Towerops.Repo.preload(invitation, :invited_by))})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'email' and 'role' parameters"})
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
invitation = Towerops.Repo.get!(Towerops.Organizations.Invitation, id)
|
||||
|
||||
if invitation.organization_id == organization_id do
|
||||
case Organizations.delete_invitation(invitation) do
|
||||
{:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "")
|
||||
{:error, _} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Could not delete invitation"})
|
||||
end
|
||||
else
|
||||
conn |> put_status(:not_found) |> json(%{error: "Invitation not found"})
|
||||
end
|
||||
rescue
|
||||
Ecto.NoResultsError ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Invitation not found"})
|
||||
end
|
||||
|
||||
defp format_invitation(invitation) do
|
||||
%{
|
||||
id: invitation.id,
|
||||
email: invitation.email,
|
||||
role: invitation.role,
|
||||
expires_at: invitation.expires_at,
|
||||
invited_by_email: get_in_loaded(invitation, :invited_by, :email),
|
||||
inserted_at: invitation.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
defp get_in_loaded(struct, assoc, field) do
|
||||
case Map.get(struct, assoc) do
|
||||
%Ecto.Association.NotLoaded{} -> nil
|
||||
nil -> nil
|
||||
loaded -> Map.get(loaded, field)
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
71
lib/towerops_web/controllers/api/v1/members_controller.ex
Normal file
71
lib/towerops_web/controllers/api/v1/members_controller.ex
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
defmodule ToweropsWeb.Api.V1.MembersController do
|
||||
@moduledoc "API controller for organization members."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Organizations
|
||||
|
||||
def index(conn, _params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
members =
|
||||
organization_id
|
||||
|> Organizations.list_organization_members()
|
||||
|> Enum.map(&format_member/1)
|
||||
|
||||
json(conn, %{data: members})
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => user_id, "role" => role}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Organizations.update_member_role(organization_id, user_id, role) do
|
||||
{:ok, membership} ->
|
||||
membership = Towerops.Repo.preload(membership, :user)
|
||||
json(conn, %{data: format_member(membership)})
|
||||
|
||||
{:error, :cannot_change_owner_role} ->
|
||||
conn |> put_status(:forbidden) |> json(%{error: "Cannot change owner role"})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Member not found"})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'role' parameter"})
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => user_id}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
|
||||
case Organizations.remove_member(organization_id, user_id) do
|
||||
{:ok, _} -> conn |> put_status(:no_content) |> send_resp(204, "")
|
||||
{:error, :cannot_remove_owner} -> conn |> put_status(:forbidden) |> json(%{error: "Cannot remove owner"})
|
||||
{:error, :not_found} -> conn |> put_status(:not_found) |> json(%{error: "Member not found"})
|
||||
end
|
||||
end
|
||||
|
||||
defp format_member(membership) do
|
||||
%{
|
||||
id: membership.user_id,
|
||||
email: membership.user.email,
|
||||
role: membership.role,
|
||||
inserted_at: membership.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
defmodule ToweropsWeb.Api.V1.OrganizationController do
|
||||
@moduledoc "API controller for current organization settings."
|
||||
use ToweropsWeb, :controller
|
||||
|
||||
alias Towerops.Organizations
|
||||
|
||||
def show(conn, _params) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
org = Organizations.get_organization!(organization_id)
|
||||
json(conn, %{data: format_org(org)})
|
||||
end
|
||||
|
||||
def update(conn, %{"organization" => attrs}) do
|
||||
organization_id = conn.assigns.current_organization_id
|
||||
org = Organizations.get_organization!(organization_id)
|
||||
|
||||
case Organizations.update_organization(org, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, %{data: format_org(updated)})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: translate_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, _params) do
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Missing 'organization' parameter"})
|
||||
end
|
||||
|
||||
defp format_org(org) do
|
||||
%{
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
subscription_plan: org.subscription_plan,
|
||||
use_sites: org.use_sites,
|
||||
snmp_version: org.snmp_version,
|
||||
snmp_community: org.snmp_community,
|
||||
snmp_port: org.snmp_port,
|
||||
snmp_transport: org.snmp_transport,
|
||||
snmpv3_security_level: org.snmpv3_security_level,
|
||||
snmpv3_username: org.snmpv3_username,
|
||||
snmpv3_auth_protocol: org.snmpv3_auth_protocol,
|
||||
snmpv3_priv_protocol: org.snmpv3_priv_protocol,
|
||||
mikrotik_enabled: org.mikrotik_enabled,
|
||||
mikrotik_username: org.mikrotik_username,
|
||||
mikrotik_port: org.mikrotik_port,
|
||||
mikrotik_ssh_port: org.mikrotik_ssh_port,
|
||||
mikrotik_use_ssl: org.mikrotik_use_ssl,
|
||||
default_agent_token_id: org.default_agent_token_id,
|
||||
inserted_at: org.inserted_at,
|
||||
updated_at: org.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
defp translate_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
|
||||
if String.length(key) <= 50 and String.match?(key, ~r/^[a-z_]+$/) do
|
||||
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -123,6 +123,27 @@ defmodule ToweropsWeb.Router do
|
|||
|
||||
resources "/sites", SitesController, except: [:new, :edit]
|
||||
resources "/devices", DevicesController, except: [:new, :edit]
|
||||
|
||||
resources "/agents", AgentsController, except: [:new, :edit]
|
||||
|
||||
resources "/alerts", AlertsController, only: [:index, :show]
|
||||
post "/alerts/:id/acknowledge", AlertsController, :acknowledge
|
||||
post "/alerts/:id/resolve", AlertsController, :resolve
|
||||
|
||||
get "/organization", OrganizationController, :show
|
||||
patch "/organization", OrganizationController, :update
|
||||
|
||||
resources "/members", MembersController, only: [:index, :delete, :update]
|
||||
resources "/invitations", InvitationsController, only: [:index, :create, :delete]
|
||||
|
||||
resources "/integrations", IntegrationsController, except: [:new, :edit]
|
||||
post "/integrations/:id/test", IntegrationsController, :test_connection
|
||||
|
||||
get "/devices/:device_id/checks", CheckResultsController, :checks
|
||||
get "/devices/:device_id/metrics", CheckResultsController, :metrics
|
||||
get "/devices/:device_id/interfaces", CheckResultsController, :interfaces
|
||||
|
||||
get "/activity", ActivityController, :index
|
||||
end
|
||||
|
||||
# Webhook routes (shared secret authentication)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue