mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat: add api for acl management (#171)
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"name": "wanderer-dev",
|
||||
"dockerComposeFile": ["./docker-compose.yml"],
|
||||
"extensions": ["jakebecker.elixir-ls"],
|
||||
"extensions": [
|
||||
"jakebecker.elixir-ls",
|
||||
"JakeBecker.elixir-ls",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"service": "wanderer",
|
||||
"workspaceFolder": "/app",
|
||||
"shutdownAction": "stopCompose",
|
||||
|
||||
@@ -7,4 +7,5 @@ export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
|
||||
export GIT_SHA="1111"
|
||||
export WANDERER_INVITES="false"
|
||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||
export WANDERER_CHARACTER_API_DISABLED="false"
|
||||
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
||||
|
||||
BIN
assets/static/images/news/02-20-acl-api/generate-acl-key.png
Executable file
BIN
assets/static/images/news/02-20-acl-api/generate-acl-key.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -53,6 +53,11 @@ public_api_disabled =
|
||||
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
character_api_disabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_CHARACTER_API_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
zkill_preload_disabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|
||||
@@ -123,6 +128,7 @@ config :wanderer_app,
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
public_api_disabled: public_api_disabled,
|
||||
character_api_disabled: character_api_disabled,
|
||||
zkill_preload_disabled: zkill_preload_disabled,
|
||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
code_interface do
|
||||
define(:create, action: :create)
|
||||
|
||||
define(:available, action: :available)
|
||||
define(:new, action: :new)
|
||||
define(:read, action: :read)
|
||||
@@ -39,7 +38,8 @@ defmodule WandererApp.Api.AccessList do
|
||||
end
|
||||
|
||||
create :new do
|
||||
accept [:name, :description, :owner_id]
|
||||
# Added :api_key to the accepted attributes
|
||||
accept [:name, :description, :owner_id, :api_key]
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id, :uuid, allow_nil?: false
|
||||
@@ -48,7 +48,7 @@ defmodule WandererApp.Api.AccessList do
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [:name, :description, :owner_id]
|
||||
accept [:name, :description, :owner_id, :api_key]
|
||||
primary?(true)
|
||||
end
|
||||
|
||||
@@ -68,6 +68,10 @@ defmodule WandererApp.Api.AccessList do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :api_key, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ defmodule WandererApp.Env do
|
||||
def invites, do: get_key(:invites, false)
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||
def character_api_disabled?, do: get_key(:character_api_disabled, false)
|
||||
def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins, do: get_key(:admins, [])
|
||||
|
||||
181
lib/wanderer_app_web/controllers/access_list_api_controller.ex
Normal file
181
lib/wanderer_app_web/controllers/access_list_api_controller.ex
Normal file
@@ -0,0 +1,181 @@
|
||||
defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
@moduledoc """
|
||||
API endpoints for managing Access Lists.
|
||||
|
||||
Endpoints:
|
||||
- GET /api/map/acls?map_id=... or ?slug=... (list ACLs)
|
||||
- POST /api/map/acls (create ACL)
|
||||
- GET /api/acls/:id (show ACL)
|
||||
- PUT /api/acls/:id (update ACL)
|
||||
|
||||
ACL members are managed via a separate controller.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
alias WandererApp.Api.{AccessList, Character}
|
||||
# Do not alias Map—to avoid conflicts—use the full module name: WandererApp.Map.
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
import Ash.Query
|
||||
|
||||
# List ACLs for a given map (returns reduced info: no api_key, no members, and includes owner_eve_id)
|
||||
def index(conn, params) do
|
||||
case Util.fetch_map_id(params) do
|
||||
{:ok, map_identifier} ->
|
||||
with {:ok, map} <- get_map(map_identifier) do
|
||||
acls = map.acls || []
|
||||
json(conn, %{data: Enum.map(acls, &acl_to_list_json/1)})
|
||||
else
|
||||
{:error, :map_not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Map not found"})
|
||||
{:error, error} ->
|
||||
conn |> put_status(:internal_server_error) |> json(%{error: inspect(error)})
|
||||
end
|
||||
{:error, msg} ->
|
||||
conn |> put_status(:bad_request) |> json(%{error: msg})
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new ACL for a map
|
||||
def create(conn, params) do
|
||||
with {:ok, map_identifier} <- Util.fetch_map_id(params),
|
||||
{:ok, map} <- get_map(map_identifier),
|
||||
%{"acl" => acl_params} <- params,
|
||||
owner_eve_id when is_binary(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
||||
{:ok, character} <- find_character_by_eve_id(owner_eve_id),
|
||||
{:ok, new_api_key} <- {:ok, UUID.uuid4()},
|
||||
{:ok, new_params} <- {:ok,
|
||||
acl_params
|
||||
|> Map.delete("owner_eve_id")
|
||||
|> Map.put("owner_id", character.id)
|
||||
|> Map.put("api_key", new_api_key)
|
||||
},
|
||||
{:ok, new_acl} <- AccessList.new(new_params),
|
||||
{:ok, _} <- {:ok, associate_acl_with_map(map, new_acl)}
|
||||
do
|
||||
json(conn, %{data: acl_to_json(new_acl)})
|
||||
else
|
||||
error ->
|
||||
conn |> put_status(:bad_request) |> json(%{error: inspect(error)})
|
||||
end
|
||||
end
|
||||
|
||||
# Show a specific ACL (with members)
|
||||
def show(conn, %{"id" => id}) do
|
||||
query = AccessList |> Ash.Query.new() |> filter(id == ^id)
|
||||
case WandererApp.Api.read(query) do
|
||||
{:ok, [acl]} ->
|
||||
case Ash.load(acl, :members) do
|
||||
{:ok, loaded_acl} -> json(conn, %{data: acl_to_json(loaded_acl)})
|
||||
{:error, error} -> conn |> put_status(:internal_server_error) |> json(%{error: "Failed to load ACL members: #{inspect(error)}"})
|
||||
end
|
||||
{:ok, []} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "ACL not found"})
|
||||
{:error, error} ->
|
||||
conn |> put_status(:internal_server_error) |> json(%{error: "Error reading ACL: #{inspect(error)}"})
|
||||
end
|
||||
end
|
||||
|
||||
# Update an ACL (if needed)
|
||||
def update(conn, %{"id" => id, "acl" => acl_params}) do
|
||||
with {:ok, acl} <- AccessList.by_id(id),
|
||||
{:ok, updated_acl} <- AccessList.update(acl, acl_params),
|
||||
{:ok, updated_acl} <- Ash.load(updated_acl, :members) do
|
||||
json(conn, %{data: acl_to_json(updated_acl)})
|
||||
else
|
||||
{:error, error} ->
|
||||
conn |> put_status(:bad_request) |> json(%{error: "Failed to update ACL: #{inspect(error)}"})
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to get the map (using your module WandererApp.Map)
|
||||
defp get_map(map_identifier) do
|
||||
# Assuming Util.fetch_map_id returns a map id.
|
||||
case WandererApp.Map.get_map(map_identifier) do
|
||||
{:ok, map} -> {:ok, map}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to convert an ACL to full JSON (for detail views)
|
||||
defp acl_to_json(acl) do
|
||||
members =
|
||||
case acl.members do
|
||||
%Ash.NotLoaded{} -> []
|
||||
list when is_list(list) -> Enum.map(list, &member_to_json/1)
|
||||
_ -> []
|
||||
end
|
||||
%{
|
||||
id: acl.id,
|
||||
name: acl.name,
|
||||
description: acl.description,
|
||||
owner_id: acl.owner_id,
|
||||
api_key: acl.api_key,
|
||||
inserted_at: acl.inserted_at,
|
||||
updated_at: acl.updated_at,
|
||||
members: members
|
||||
}
|
||||
end
|
||||
|
||||
defp acl_to_list_json(acl) do
|
||||
full_acl =
|
||||
case AccessList.by_id(acl.id) do
|
||||
{:ok, loaded_acl} -> loaded_acl
|
||||
_ -> acl
|
||||
end
|
||||
|
||||
owner_eve_id =
|
||||
case find_character_by_id(full_acl.owner_id) do
|
||||
{:ok, character} -> character.eve_id
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
%{
|
||||
id: full_acl.id,
|
||||
name: full_acl.name,
|
||||
description: full_acl.description,
|
||||
owner_eve_id: owner_eve_id,
|
||||
inserted_at: full_acl.inserted_at,
|
||||
updated_at: full_acl.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
defp member_to_json(member) do
|
||||
%{
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
eve_character_id: member.eve_character_id,
|
||||
inserted_at: member.inserted_at,
|
||||
updated_at: member.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
# Helper to find a character by external EVE id (used in create action)
|
||||
defp find_character_by_eve_id(eve_id) do
|
||||
query = Character |> Ash.Query.new() |> filter(eve_id == ^eve_id)
|
||||
case WandererApp.Api.read(query) do
|
||||
{:ok, [character]} -> {:ok, character}
|
||||
{:ok, []} -> {:error, "owner_eve_id does not match any existing character"}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to find a character by internal id (used in acl_to_list_json)
|
||||
defp find_character_by_id(id) do
|
||||
query = Character |> Ash.Query.new() |> filter(id == ^id)
|
||||
case WandererApp.Api.read(query) do
|
||||
{:ok, [character]} -> {:ok, character}
|
||||
{:ok, []} -> {:error, "Character not found"}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
# Associate the new ACL with the map by updating the map's acls list.
|
||||
defp associate_acl_with_map(map, new_acl) do
|
||||
current_acls = map.acls || []
|
||||
updated_acls = current_acls ++ [new_acl]
|
||||
case WandererApp.Map.update_map(map.map_id, %{acls: updated_acls}) do
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
@moduledoc """
|
||||
Handles creation, role updates, and deletion of individual ACL members.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
alias WandererApp.Api.{AccessListMember, Character}
|
||||
import Ash.Query
|
||||
|
||||
@doc """
|
||||
POST /api/acls/:acl_id/members
|
||||
|
||||
Creates a new member for the given ACL.
|
||||
|
||||
Request Body example:
|
||||
{
|
||||
"member": {
|
||||
"eve_character_id": "CHARACTER_EXTERNAL_EVE_ID",
|
||||
"role": "viewer" // optional; defaults to "viewer" if not provided
|
||||
}
|
||||
}
|
||||
|
||||
Behavior:
|
||||
The controller looks up the character by filtering on its external EVE ID (eve_id),
|
||||
injects the character's name into the membership, and creates the membership record.
|
||||
"""
|
||||
def create(conn, %{"acl_id" => acl_id, "member" => member_params}) do
|
||||
with eve_id when not is_nil(eve_id) <- Map.get(member_params, "eve_character_id"),
|
||||
# Build a query to find the character by its external EVE id (eve_id)
|
||||
query = Character |> Ash.Query.new() |> filter(eve_id == ^eve_id),
|
||||
{:ok, characters} <- WandererApp.Api.read(query),
|
||||
[character] <- characters do
|
||||
# Inject the looked-up name into the parameters.
|
||||
member_params = Map.put(member_params, "name", character.name)
|
||||
# Merge in the ACL id so that Ash knows which ACL the member belongs to.
|
||||
merged_params = Map.put(member_params, "access_list_id", acl_id)
|
||||
|
||||
case AccessListMember.create(merged_params) do
|
||||
{:ok, new_member} ->
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Failed to create member: #{inspect(error)}"})
|
||||
end
|
||||
else
|
||||
nil ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing eve_character_id in member payload"})
|
||||
|
||||
{:ok, []} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Character not found"})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Character lookup failed: #{inspect(error)}"})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
PUT /api/acls/:acl_id/members/:member_id
|
||||
|
||||
Updates a single ACL member’s role based on the external EVE ID provided in the URL.
|
||||
|
||||
Request Body example:
|
||||
{
|
||||
"member": {
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
"""
|
||||
def update_role(conn, %{"acl_id" => acl_id, "member_id" => eve_id, "member" => member_params}) do
|
||||
membership_query =
|
||||
AccessListMember
|
||||
|> Ash.Query.new()
|
||||
|> filter(eve_character_id == ^eve_id)
|
||||
|> filter(access_list_id == ^acl_id)
|
||||
|
||||
case WandererApp.Api.read(membership_query) do
|
||||
{:ok, [membership]} ->
|
||||
case AccessListMember.update_role(membership, member_params) do
|
||||
{:ok, updated_membership} ->
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: inspect(error)})
|
||||
end
|
||||
|
||||
{:ok, []} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Membership not found for given ACL and eve_character_id"})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: inspect(error)})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
DELETE /api/acls/:acl_id/members/:member_id
|
||||
|
||||
Deletes a member from an ACL based on the external EVE ID provided in the URL.
|
||||
"""
|
||||
def delete(conn, %{"acl_id" => acl_id, "member_id" => eve_id}) do
|
||||
membership_query =
|
||||
AccessListMember
|
||||
|> Ash.Query.new()
|
||||
|> filter(eve_character_id == ^eve_id)
|
||||
|> filter(access_list_id == ^acl_id)
|
||||
|
||||
case WandererApp.Api.read(membership_query) do
|
||||
{:ok, [membership]} ->
|
||||
case AccessListMember.destroy(membership) do
|
||||
:ok ->
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: inspect(error)})
|
||||
end
|
||||
|
||||
{:ok, []} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Membership not found for given ACL and eve_character_id"})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: inspect(error)})
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
defp member_to_json(member) do
|
||||
%{
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
eve_character_id: member.eve_character_id,
|
||||
inserted_at: member.inserted_at,
|
||||
updated_at: member.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
39
lib/wanderer_app_web/controllers/character_api_controller.ex
Normal file
39
lib/wanderer_app_web/controllers/character_api_controller.ex
Normal file
@@ -0,0 +1,39 @@
|
||||
defmodule WandererAppWeb.CharactersAPIController do
|
||||
@moduledoc """
|
||||
Exposes an endpoint for listing ALL characters in the database
|
||||
|
||||
Endpoint:
|
||||
GET /api/characters
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
alias WandererApp.Api.Character
|
||||
|
||||
@doc """
|
||||
GET /api/characters
|
||||
|
||||
Lists ALL characters in the database
|
||||
Returns an array of objects, each with `id`, `eve_id`, `name`, etc.
|
||||
"""
|
||||
def index(conn, _params) do
|
||||
case WandererApp.Api.read(Character) do
|
||||
{:ok, characters} ->
|
||||
result =
|
||||
characters
|
||||
|> Enum.map(&%{
|
||||
id: &1.id,
|
||||
eve_id: &1.eve_id,
|
||||
name: &1.name,
|
||||
corporation_name: &1.corporation_name,
|
||||
alliance_name: &1.alliance_name
|
||||
})
|
||||
|
||||
json(conn, %{data: result})
|
||||
|
||||
{:error, error} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: inspect(error)})
|
||||
end
|
||||
end
|
||||
end
|
||||
55
lib/wanderer_app_web/controllers/plugs/check_acl_api_key.ex
Normal file
55
lib/wanderer_app_web/controllers/plugs/check_acl_api_key.ex
Normal file
@@ -0,0 +1,55 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckAclApiKey do
|
||||
@moduledoc """
|
||||
A plug that checks the "Authorization: Bearer <token>" header
|
||||
against the ACL’s stored api_key.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
alias WandererApp.Repo
|
||||
alias WandererApp.Api.AccessList
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
header = get_req_header(conn, "authorization") |> List.first()
|
||||
|
||||
case header do
|
||||
"Bearer " <> incoming_token ->
|
||||
acl_id = conn.params["id"] || conn.params["acl_id"]
|
||||
|
||||
if acl_id do
|
||||
case Repo.get(AccessList, acl_id) do
|
||||
nil ->
|
||||
conn
|
||||
|> send_resp(404, "ACL not found")
|
||||
|> halt()
|
||||
|
||||
acl ->
|
||||
cond do
|
||||
is_nil(acl.api_key) ->
|
||||
conn
|
||||
|> send_resp(401, "Unauthorized (no API key set for ACL)")
|
||||
|> halt()
|
||||
|
||||
acl.api_key == incoming_token ->
|
||||
conn
|
||||
|
||||
true ->
|
||||
conn
|
||||
|> send_resp(401, "Unauthorized (invalid API key for ACL)")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
else
|
||||
conn
|
||||
|> send_resp(400, "ACL ID not provided")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
_ ->
|
||||
conn
|
||||
|> send_resp(401, "Missing or invalid 'Bearer' token")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckCharacterApiDisabled do
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
if WandererApp.Env.character_api_disabled?() do
|
||||
conn
|
||||
|> send_resp(403, "Character API is disabled")
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
@@ -37,10 +38,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
end
|
||||
|
||||
defp fetch_map(query_params) do
|
||||
case fetch_map_id(query_params) do
|
||||
{:ok, {:map, map}} ->
|
||||
{:ok, map}
|
||||
|
||||
case Util.fetch_map_id(query_params) do
|
||||
{:ok, map_id} ->
|
||||
WandererApp.Api.Map.by_id(map_id)
|
||||
|
||||
@@ -48,20 +46,4 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
||||
{:ok, mid}
|
||||
end
|
||||
|
||||
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} ->
|
||||
{:ok, {:map, map}}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, "No map found for slug=#{slug}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
||||
end
|
||||
|
||||
@@ -314,6 +314,24 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("generate-api-key", _params, socket) do
|
||||
new_api_key = UUID.uuid4()
|
||||
new_params = Map.put(socket.assigns.form.params || %{}, "api_key", new_api_key)
|
||||
form = AshPhoenix.Form.validate(socket.assigns.form, new_params)
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("noop", _, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{"update_role", %{member_id: member_id, role: role}},
|
||||
@@ -328,17 +346,6 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, socket |> maybe_update_role(member, role_atom, access_list)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("noop", _, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:search, text}, socket) do
|
||||
active_character_id =
|
||||
|
||||
@@ -142,6 +142,50 @@
|
||||
placeholder="Select an owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
|
||||
<!-- Divider between above inputs and the API key section -->
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="mt-2">
|
||||
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-7">
|
||||
<.input
|
||||
type="text"
|
||||
field={f[:api_key]}
|
||||
placeholder="No API Key yet"
|
||||
readonly
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="generate-api-key"
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label">Generate</span>
|
||||
</.button>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<.button
|
||||
type="button"
|
||||
phx-hook="CopyToClipboard"
|
||||
id="copy-acl-api-key"
|
||||
data-url={f[:api_key].value}
|
||||
disabled={is_nil(f[:api_key].value) or f[:api_key].value == ""}
|
||||
class={"p-button p-component w-full " <> if(is_nil(f[:api_key].value) or f[:api_key].value == "", do: "p-disabled", else: "")}
|
||||
>
|
||||
<span class="p-button-label">Copy</span>
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
<div class="modal-action">
|
||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||
<%= (@live_action == :create && "Create") || "Save" %>
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
<% end %>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<.modal
|
||||
:if={@is_connected? && @live_action in [:create, :edit]}
|
||||
title={"#{(@live_action == :create && "Create") || "Edit"} Map"}
|
||||
@@ -256,9 +257,7 @@
|
||||
:if={@map_subscriptions_enabled?}
|
||||
class={[
|
||||
"p-unselectable-text",
|
||||
classes(
|
||||
"p-tabview-selected p-highlight": @active_settings_tab == "subscription"
|
||||
)
|
||||
classes("p-tabview-selected p-highlight": @active_settings_tab == "subscription")
|
||||
]}
|
||||
role="presentation"
|
||||
data-pc-name=""
|
||||
@@ -310,9 +309,7 @@
|
||||
:if={not WandererApp.Env.public_api_disabled?()}
|
||||
class={[
|
||||
"p-unselectable-text",
|
||||
classes(
|
||||
"p-tabview-selected p-highlight": @active_settings_tab == "public_api"
|
||||
)
|
||||
classes("p-tabview-selected p-highlight": @active_settings_tab == "public_api")
|
||||
]}
|
||||
role="presentation"
|
||||
data-pc-name=""
|
||||
@@ -414,10 +411,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={
|
||||
@active_settings_tab == "public_api" and
|
||||
not WandererApp.Env.public_api_disabled?()
|
||||
}
|
||||
:if={@active_settings_tab == "public_api" and not WandererApp.Env.public_api_disabled?()}
|
||||
class="p-6"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-4">Public API</h2>
|
||||
@@ -439,29 +433,28 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<.button class="btn btn-primary rounded-md" phx-click="generate-map-api-key">
|
||||
Generate
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="generate-map-api-key"
|
||||
class="p-button p-component p-button-primary"
|
||||
style="min-width: 120px;"
|
||||
>
|
||||
<span class="p-button-label">Generate</span>
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
phx-hook="CopyToClipboard"
|
||||
id="copy-map-api-key"
|
||||
data-url={@public_api_key}
|
||||
disabled={is_nil(@public_api_key)}
|
||||
class={
|
||||
if is_nil(@public_api_key) do
|
||||
"copy-link btn rounded-md transition-colors duration-300
|
||||
bg-gray-500 hover:bg-gray-500 text-gray-300 cursor-not-allowed"
|
||||
else
|
||||
"copy-link btn rounded-md transition-colors duration-300
|
||||
bg-blue-600 hover:bg-blue-700 text-white cursor-pointer"
|
||||
end
|
||||
}
|
||||
class={"p-button p-component " <> if(is_nil(@public_api_key), do: "p-disabled", else: "")}
|
||||
>
|
||||
Copy
|
||||
<span class="p-button-label">Copy</span>
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={@active_settings_tab == "balance"}>
|
||||
<div class="stats w-full bg-primary text-primary-content">
|
||||
<div class="stat">
|
||||
@@ -687,10 +680,8 @@
|
||||
<.button
|
||||
:if={@active_settings_tab == "subscription" && not @is_adding_subscription?}
|
||||
type="button"
|
||||
disabled={
|
||||
@map_subscriptions |> Enum.at(0) |> Map.get(:status) == :active &&
|
||||
@map_subscriptions |> Enum.at(0) |> Map.get(:plan) != :alpha
|
||||
}
|
||||
disabled={@map_subscriptions |> Enum.at(0) |> Map.get(:status) == :active &&
|
||||
@map_subscriptions |> Enum.at(0) |> Map.get(:plan) != :alpha}
|
||||
phx-click="add_subscription"
|
||||
>
|
||||
Add subscription
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererAppWeb.Router do
|
||||
use Plug.ErrorHandler
|
||||
|
||||
import PlugDynamic.Builder
|
||||
import Logger
|
||||
|
||||
import WandererAppWeb.UserAuth,
|
||||
warn: false,
|
||||
@@ -118,6 +119,14 @@ defmodule WandererAppWeb.Router do
|
||||
plug WandererAppWeb.Plugs.CheckApiDisabled
|
||||
end
|
||||
|
||||
pipeline :api_character do
|
||||
plug WandererAppWeb.Plugs.CheckCharacterApiDisabled
|
||||
end
|
||||
|
||||
pipeline :api_acl do
|
||||
plug WandererAppWeb.Plugs.CheckAclApiKey
|
||||
end
|
||||
|
||||
scope "/api/map/systems-kills", WandererAppWeb do
|
||||
pipe_through [:api, :api_map, :api_kills]
|
||||
|
||||
@@ -126,66 +135,77 @@ defmodule WandererAppWeb.Router do
|
||||
|
||||
scope "/api/map", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
|
||||
# GET /api/map/systems?map_id=... or ?slug=...
|
||||
get "/systems", MapAPIController, :list_systems
|
||||
|
||||
# GET /api/map/system?id=... plus either map_id=... or slug=...
|
||||
get "/system", MapAPIController, :show_system
|
||||
|
||||
# GET /api/map/characters?map_id=... or slug=...
|
||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||
|
||||
# GET /api/map/structure-timers?map_id=... or slug=... and optionally ?system_id=...
|
||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||
get "/acls", MapAccessListAPIController, :index
|
||||
post "/acls", MapAccessListAPIController, :create
|
||||
end
|
||||
|
||||
|
||||
scope "/api/characters", WandererAppWeb do
|
||||
pipe_through [:api, :api_character]
|
||||
get "/", CharactersAPIController, :index
|
||||
end
|
||||
|
||||
scope "/api/acls", WandererAppWeb do
|
||||
pipe_through [:api, :api_acl]
|
||||
|
||||
get "/:id", MapAccessListAPIController, :show
|
||||
put "/:id", MapAccessListAPIController, :update
|
||||
post "/:acl_id/members", AccessListMemberAPIController, :create
|
||||
put "/:acl_id/members/:member_id", AccessListMemberAPIController, :update_role
|
||||
delete "/:acl_id/members/:member_id", AccessListMemberAPIController, :delete
|
||||
end
|
||||
|
||||
scope "/api/common", WandererAppWeb do
|
||||
pipe_through [:api]
|
||||
|
||||
# GET /api/common/system-static-info?id=...
|
||||
get "/system-static-info", CommonAPIController, :show_system_static
|
||||
end
|
||||
|
||||
#
|
||||
# Browser / blog stuff
|
||||
#
|
||||
scope "/", WandererAppWeb do
|
||||
pipe_through [:browser, :blog, :redirect_if_user_is_authenticated]
|
||||
|
||||
get "/welcome", BlogController, :index
|
||||
end
|
||||
|
||||
scope "/contacts", WandererAppWeb do
|
||||
pipe_through [:browser, :blog]
|
||||
|
||||
get "/", BlogController, :contacts
|
||||
end
|
||||
|
||||
scope "/changelog", WandererAppWeb do
|
||||
pipe_through [:browser, :blog]
|
||||
|
||||
get "/", BlogController, :changelog
|
||||
end
|
||||
|
||||
scope "/news", WandererAppWeb do
|
||||
pipe_through [:browser, :blog]
|
||||
|
||||
get "/:slug", BlogController, :show
|
||||
get "/", BlogController, :list
|
||||
end
|
||||
|
||||
scope "/license", WandererAppWeb do
|
||||
pipe_through [:browser, :blog]
|
||||
|
||||
get "/", BlogController, :license
|
||||
end
|
||||
|
||||
#
|
||||
# Auth
|
||||
#
|
||||
scope "/auth", WandererAppWeb do
|
||||
pipe_through :browser
|
||||
|
||||
get "/signout", AuthController, :signout
|
||||
get "/:provider", AuthController, :request
|
||||
get "/:provider/callback", AuthController, :callback
|
||||
end
|
||||
|
||||
#
|
||||
# Admin
|
||||
#
|
||||
scope "/admin", WandererAppWeb do
|
||||
pipe_through(:browser)
|
||||
pipe_through(:admin_bauth)
|
||||
@@ -207,53 +227,49 @@ defmodule WandererAppWeb.Router do
|
||||
)
|
||||
end
|
||||
|
||||
#
|
||||
# Additional routes / Live sessions
|
||||
#
|
||||
scope "/", WandererAppWeb do
|
||||
pipe_through(:browser)
|
||||
|
||||
get "/", RedirectController, :redirect_authenticated
|
||||
get("/last", MapsController, :last)
|
||||
get "/last", MapsController, :last
|
||||
|
||||
live_session :authenticated,
|
||||
on_mount: [
|
||||
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
||||
WandererAppWeb.Nav
|
||||
] do
|
||||
live("/access-lists/new", AccessListsLive, :create)
|
||||
live("/access-lists/:id/edit", AccessListsLive, :edit)
|
||||
live("/access-lists/:id/add-members", AccessListsLive, :add_members)
|
||||
live("/access-lists/:id", AccessListsLive, :members)
|
||||
live("/access-lists", AccessListsLive, :index)
|
||||
live("/coming-soon", ComingLive, :index)
|
||||
live("/tracking/:slug", CharactersTrackingLive, :characters)
|
||||
live("/tracking", CharactersTrackingLive, :index)
|
||||
live("/characters", CharactersLive, :index)
|
||||
live("/characters/authorize", CharactersLive, :authorize)
|
||||
live("/maps/new", MapsLive, :create)
|
||||
live("/maps/:slug/edit", MapsLive, :edit)
|
||||
live("/maps/:slug/settings", MapsLive, :settings)
|
||||
live("/maps", MapsLive, :index)
|
||||
live("/profile", ProfileLive, :index)
|
||||
live("/profile/deposit", ProfileLive, :deposit)
|
||||
live("/profile/subscribe", ProfileLive, :subscribe)
|
||||
live("/:slug/audit", MapAuditLive, :index)
|
||||
live("/:slug", MapLive, :index)
|
||||
live "/access-lists/new", AccessListsLive, :create
|
||||
live "/access-lists/:id/edit", AccessListsLive, :edit
|
||||
live "/access-lists/:id/add-members", AccessListsLive, :add_members
|
||||
live "/access-lists/:id", AccessListsLive, :members
|
||||
live "/access-lists", AccessListsLive, :index
|
||||
|
||||
live "/coming-soon", ComingLive, :index
|
||||
live "/tracking/:slug", CharactersTrackingLive, :characters
|
||||
live "/tracking", CharactersTrackingLive, :index
|
||||
live "/characters", CharactersLive, :index
|
||||
live "/characters/authorize", CharactersLive, :authorize
|
||||
live "/maps/new", MapsLive, :create
|
||||
live "/maps/:slug/edit", MapsLive, :edit
|
||||
live "/maps/:slug/settings", MapsLive, :settings
|
||||
live "/maps", MapsLive, :index
|
||||
live "/profile", ProfileLive, :index
|
||||
live "/profile/deposit", ProfileLive, :deposit
|
||||
live "/profile/subscribe", ProfileLive, :subscribe
|
||||
live "/:slug/audit", MapAuditLive, :index
|
||||
live "/:slug", MapLive, :index
|
||||
end
|
||||
end
|
||||
|
||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||
if Application.compile_env(:wanderer_app, :dev_routes) do
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
# you can use Plug.BasicAuth to set up some basic authentication
|
||||
# as long as you are also using SSL (which you should anyway).
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
scope "/dev" do
|
||||
pipe_through(:browser)
|
||||
|
||||
error_tracker_dashboard("/errors", as: :error_tracker_dev_dashboard)
|
||||
|
||||
live_dashboard("/dashboard", metrics: WandererAppWeb.Telemetry)
|
||||
end
|
||||
end
|
||||
|
||||
425
priv/posts/2025/02-20-acl-api.md
Normal file
425
priv/posts/2025/02-20-acl-api.md
Normal file
@@ -0,0 +1,425 @@
|
||||
%{
|
||||
title: "User Guide: Characters & ACL API Endpoints",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/02-20-acl-api/generate-key.png",
|
||||
tags: ~w(acl characters guide interface),
|
||||
description: "Learn how to retrieve and manage Access Lists and Characters through the Wanderer public APIs. This guide covers available endpoints, request examples, and sample responses."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Wanderer’s expanded public API now lets you retrieve **all characters** in the system and manage “Access Lists” (ACLs) for controlling visibility or permissions. These endpoints allow you to:
|
||||
|
||||
- Fetch a list of **all** EVE characters known to the system.
|
||||
- List ACLs for a given map.
|
||||
- Create new ACLs for maps (with automatic API key generation).
|
||||
- Update existing ACLs.
|
||||
- Add, remove, and change the roles of ACL members.
|
||||
|
||||
This guide provides step-by-step instructions, request/response examples, and details on how to authenticate each call.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Unless otherwise noted, these endpoints require a valid **Bearer** token. Pass it in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
Authorization: Bearer <REDACTED_TOKEN>
|
||||
```
|
||||
|
||||
If the token is missing or invalid, you’ll receive a `401 Unauthorized` error.
|
||||
_(No API key is required for some “common” endpoints, but ACL- and character-related endpoints require a valid token.)_
|
||||
|
||||
There are two types of tokens in use:
|
||||
|
||||
1. **Map API Token:** Available in the map settings. This token is used for map-specific endpoints (e.g. listing ACLs for a map and creating ACLs).
|
||||
|
||||

|
||||
|
||||
2. **ACL API Token:** Available in the create/edit ACL screen. This token is used for ACL member management endpoints.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Endpoints Overview
|
||||
|
||||
### 1. List **All** Characters
|
||||
|
||||
```bash
|
||||
GET /api/characters
|
||||
```
|
||||
|
||||
- **Description:** Returns a list of **all** characters known to Wanderer.
|
||||
- **Toggle:** Controlled by the environment variable `WANDERER_CHARACTER_API_DISABLED` (default is `false`).
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
"https://wanderer.example.com/api/characters"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "b374d9e6-47a7-4e20-85ad-d608809827b5",
|
||||
"name": "Some Character",
|
||||
"eve_id": "2122825111",
|
||||
"corporation_name": "School of Applied Knowledge",
|
||||
"alliance_name": null
|
||||
},
|
||||
{
|
||||
"id": "6963bee6-eaa1-40e2-8200-4bc2fcbd7350",
|
||||
"name": "Other Character",
|
||||
"eve_id": "2122019111",
|
||||
"corporation_name": "Some Corporation",
|
||||
"alliance_name": null
|
||||
}
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use the `eve_id` when referencing a character in ACL operations.
|
||||
|
||||
---
|
||||
|
||||
### 2. List ACLs for a Given Map
|
||||
|
||||
```bash
|
||||
GET /api/map/acls?map_id=<UUID>
|
||||
GET /api/map/acls?slug=<map-slug>
|
||||
```
|
||||
|
||||
- **Description:** Lists all ACLs associated with a map, specified by either `map_id` (UUID) or `slug`.
|
||||
- **Authentication:** Requires the Map API Token (available in map settings).
|
||||
- **Example Request (using slug):**
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
"https://wanderer.example.com/api/map/acls?slug=mapname"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "19712899-ec3a-47b1-b73b-2bae221c5513",
|
||||
"name": "aclName",
|
||||
"description": null,
|
||||
"owner_eve_id": "11111111111",
|
||||
"inserted_at": "2025-02-13T03:32:25.144403Z",
|
||||
"updated_at": "2025-02-13T03:32:25.144403Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Show a Specific ACL (Including Members)
|
||||
|
||||
```bash
|
||||
GET /api/acls/:id
|
||||
```
|
||||
|
||||
- **Description:** Fetches a single ACL by ID, with its members preloaded.
|
||||
- **Authentication:** Requires the ACL API Token.
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||
"https://wanderer.example.com/api/acls/19712899-ec3a-47b1-b73b-2bae221c5513"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "19712899-ec3a-47b1-b73b-2bae221c5513",
|
||||
"name": "aclName",
|
||||
"description": null,
|
||||
"owner_id": "d43a9083-2705-40c9-a314-f7f412346661",
|
||||
"members": [
|
||||
{
|
||||
"id": "8d63ab1e-b44f-4e81-8227-8fb8d928dad8",
|
||||
"name": "Other Character",
|
||||
"role": "admin",
|
||||
"inserted_at": "2025-02-13T03:33:32.332598Z",
|
||||
"updated_at": "2025-02-13T03:33:36.644520Z"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Create a New ACL Associated with a Map
|
||||
|
||||
```bash
|
||||
POST /api/map/acls
|
||||
```
|
||||
|
||||
- **Description:** Creates a new ACL for a map and generates a new ACL API key. The map record tracks its ACLs.
|
||||
- **Required Query Parameter:** Either `map_id` (UUID) or `slug` (map slug).
|
||||
- **Request Body Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"acl": {
|
||||
"name": "New ACL",
|
||||
"description": "Optional description",
|
||||
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `owner_eve_id` must be the external EVE id (the `eve_id` from `/api/characters`).
|
||||
- **Example Request (using map slug):**
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <MAP_API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"acl": {
|
||||
"name": "New ACL",
|
||||
"description": "Optional description",
|
||||
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||
}
|
||||
}' \
|
||||
"https://wanderer.example.com/api/map/acls?slug=mapname"
|
||||
```
|
||||
|
||||
- **Example Request (using map UUID):**
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <MAP_API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"acl": {
|
||||
"name": "New ACL",
|
||||
"description": "Optional description",
|
||||
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||
}
|
||||
}' \
|
||||
"https://wanderer.example.com/api/map/acls?map_id=YOUR_MAP_UUID"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "NEW_ACL_UUID",
|
||||
"name": "New ACL",
|
||||
"description": "Optional description",
|
||||
"owner_id": "OWNER_ID",
|
||||
"api_key": "GENERATED_ACL_API_KEY",
|
||||
"inserted_at": "2025-02-14T17:00:00Z",
|
||||
"updated_at": "2025-02-14T17:00:00Z",
|
||||
"members": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Update an ACL
|
||||
|
||||
```bash
|
||||
PUT /api/acls/:id
|
||||
```
|
||||
|
||||
- **Description:** Updates an existing ACL (e.g. name, description, api_key).
|
||||
The update endpoint fetches the ACL record first and then applies the update.
|
||||
- **Authentication:** Requires the ACL API Token.
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"acl": {
|
||||
"name": "Updated ACL Name",
|
||||
"description": "This is the updated description",
|
||||
"api_key": "EXISTING_ACL_API_KEY"
|
||||
}
|
||||
}' \
|
||||
"https://wanderer.example.com/api/acls/ACL_UUID"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "ACL_UUID",
|
||||
"name": "Updated ACL Name",
|
||||
"description": "This is the updated description",
|
||||
"owner_id": "OWNER_ID",
|
||||
"api_key": "EXISTING_ACL_API_KEY",
|
||||
"inserted_at": "2025-02-14T16:49:13.423556Z",
|
||||
"updated_at": "2025-02-14T17:22:51.343784Z",
|
||||
"members": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Add a Member to an ACL
|
||||
|
||||
```bash
|
||||
POST /api/acls/:acl_id/members
|
||||
```
|
||||
|
||||
- **Description:** Adds a new member (character, corporation, or alliance) to the specified ACL.
|
||||
- **Authentication:** Requires the ACL API Token.
|
||||
- **Request Body Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"member": {
|
||||
"name": "New Member",
|
||||
"eve_character_id": "EXTERNAL_EVE_ID",
|
||||
"role": "viewer"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `eve_character_id` is the character’s external EVE id.
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"member": {
|
||||
"name": "New Member",
|
||||
"eve_character_id": "EXTERNAL_EVE_ID",
|
||||
"role": "viewer"
|
||||
}
|
||||
}' \
|
||||
"https://wanderer.example.com/api/acls/ACL_UUID/members"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "MEMBERSHIP_UUID",
|
||||
"name": "New Member",
|
||||
"role": "viewer",
|
||||
"inserted_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Change a Member’s Role
|
||||
|
||||
```bash
|
||||
PUT /api/acls/:acl_id/members/:member_id
|
||||
```
|
||||
|
||||
- **Description:** Updates an ACL member’s role (e.g. from `viewer` to `admin`).
|
||||
The `:member_id` is the external EVE id of the character.
|
||||
- **Authentication:** Requires the ACL API Token.
|
||||
- **Request Body Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"member": {
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"member": {
|
||||
"role": "admin"
|
||||
}
|
||||
}' \
|
||||
"https://wanderer.example.com/api/acls/ACL_UUID/members/EXTERNAL_EVE_ID"
|
||||
```
|
||||
|
||||
- **Example Response (redacted):**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "MEMBERSHIP_UUID",
|
||||
"name": "New Member",
|
||||
"role": "admin",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Remove a Member from an ACL
|
||||
|
||||
```bash
|
||||
DELETE /api/acls/:acl_id/members/:member_id
|
||||
```
|
||||
|
||||
- **Description:** Removes the member with the specified external EVE id from the ACL.
|
||||
- **Authentication:** Requires the ACL API Token.
|
||||
- **Example Request:**
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||
"https://wanderer.example.com/api/acls/ACL_UUID/members/EXTERNAL_EVE_ID"
|
||||
```
|
||||
|
||||
- **Example Response:**
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This guide outlines how to:
|
||||
|
||||
1. **List** all characters (`GET /api/characters`) so you can pick a valid character to add to your ACL.
|
||||
2. **List** ACLs for a specified map (`GET /api/map/acls?map_id=<UUID>` or `?slug=<map-slug>`).
|
||||
3. **Show** ACL details, including its members (`GET /api/acls/:id`).
|
||||
4. **Create** a new ACL for a map (`POST /api/map/acls`), which generates a new ACL API key.
|
||||
5. **Update** an existing ACL (`PUT /api/acls/:id`).
|
||||
6. **Add** members (characters, corporations, alliances) to an ACL (`POST /api/acls/:acl_id/members`).
|
||||
7. **Change** a member’s role (`PUT /api/acls/:acl_id/members/:member_id`).
|
||||
8. **Remove** a member from an ACL (`DELETE /api/acls/:acl_id/members/:member_id`).
|
||||
|
||||
By following these request patterns, you can manage your ACL resources in a fully programmatic fashion. If you have any questions, feel free to reach out to the Wanderer Team.
|
||||
|
||||
Fly safe,
|
||||
**WANDERER TEAM**
|
||||
21
priv/repo/migrations/20250213182400_add_acl_api_key.exs
Normal file
21
priv/repo/migrations/20250213182400_add_acl_api_key.exs
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddAclApiKey do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:access_lists_v1) do
|
||||
add :api_key, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:access_lists_v1) do
|
||||
remove :api_key
|
||||
end
|
||||
end
|
||||
end
|
||||
108
priv/resource_snapshots/repo/access_lists_v1/20250213182400.json
Normal file
108
priv/resource_snapshots/repo/access_lists_v1/20250213182400.json
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "api_key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "access_lists_v1_owner_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "character_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "5118AF0DEBEEED63DC30565ECFFEDF682876FAD476AF2796E973C6883E4054E0",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "access_lists_v1"
|
||||
}
|
||||
Reference in New Issue
Block a user