Add api specs (#217)

This commit is contained in:
guarzo
2025-03-06 15:31:31 -05:00
committed by GitHub
parent 8e0b8fd7f9
commit c8fc31257b
19 changed files with 3071 additions and 306 deletions

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@
.env
*.local.env
test/manual/.auto*
.direnv/
.cache/

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,32 @@
defmodule WandererAppWeb.ApiSpec do
@behaviour OpenApiSpex.OpenApi
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server}
alias WandererAppWeb.{Endpoint, Router}
@impl OpenApiSpex.OpenApi
def spec do
%OpenApi{
info: %Info{
title: "WandererApp API",
version: "1.0.0",
description: "API documentation for WandererApp"
},
servers: [
Server.from_endpoint(Endpoint)
],
paths: Paths.from_router(Router),
components: %Components{
securitySchemes: %{
"bearerAuth" => %SecurityScheme{
type: "http",
scheme: "bearer",
bearerFormat: "JWT"
}
}
},
security: [%{"bearerAuth" => []}]
}
|> OpenApiSpex.resolve_schema_modules()
end
end

View File

@@ -7,26 +7,247 @@ defmodule WandererAppWeb.MapAccessListAPIController do
- 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
use OpenApiSpex.ControllerSpecs
alias WandererApp.Api.{AccessList, Character}
alias WandererAppWeb.UtilAPIController, as: Util
import Ash.Query
require Logger
# ------------------------------------------------------------------------
# Inline Schemas for OpenApiSpex
# ------------------------------------------------------------------------
# Used in operation :index => the response "List of ACLs"
@acl_index_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string},
owner_eve_id: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name"]
}
}
},
required: ["data"]
}
# Used in operation :create => the request body "ACL parameters"
@acl_create_request_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
acl: %OpenApiSpex.Schema{
type: :object,
properties: %{
owner_eve_id: %OpenApiSpex.Schema{
type: :string,
description: "EVE character ID of the owner (must match an existing character)"
},
name: %OpenApiSpex.Schema{
type: :string,
description: "Name of the access list"
},
description: %OpenApiSpex.Schema{
type: :string,
description: "Optional description of the access list"
}
},
required: ["owner_eve_id", "name"],
example: %{
"owner_eve_id" => "2112073677",
"name" => "My Access List",
"description" => "Optional description"
}
}
},
required: ["acl"]
}
# Used in operation :create => the response "Created ACL"
@acl_create_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string},
owner_id: %OpenApiSpex.Schema{type: :string},
api_key: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name"]
}
},
required: ["data"]
}
# Used in operation :show => the response "ACL details"
@acl_show_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string},
owner_id: %OpenApiSpex.Schema{type: :string},
api_key: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
members: %OpenApiSpex.Schema{
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
role: %OpenApiSpex.Schema{type: :string},
eve_character_id: %OpenApiSpex.Schema{type: :string},
eve_corporation_id: %OpenApiSpex.Schema{type: :string},
eve_alliance_id: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name", "role"]
}
}
},
required: ["id", "name"]
}
},
required: ["data"]
}
# Used in operation :update => the request body "ACL update payload"
@acl_update_request_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
acl: %OpenApiSpex.Schema{
type: :object,
properties: %{
name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string}
}
# If "name" is truly required, add it to required: ["name"] here
}
},
required: ["acl"]
}
# Used in operation :update => the response "Updated ACL"
@acl_update_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string},
owner_id: %OpenApiSpex.Schema{type: :string},
api_key: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
members: %OpenApiSpex.Schema{
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
role: %OpenApiSpex.Schema{type: :string},
eve_character_id: %OpenApiSpex.Schema{type: :string},
eve_corporation_id: %OpenApiSpex.Schema{type: :string},
eve_alliance_id: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name", "role"]
}
}
},
required: ["id", "name"]
}
},
required: ["data"]
}
# ------------------------------------------------------------------------
# ENDPOINTS
# ------------------------------------------------------------------------
@doc """
GET /api/map/acls?map_id=... or ?slug=...
Lists the ACLs for a given map.
"""
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :index,
summary: "List ACLs for a Map",
description: "Lists the ACLs for a given map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: "00000000-0000-0000-0000-000000000000"
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
responses: [
ok: {
"List of ACLs",
"application/json",
@acl_index_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
}
}}
]
def index(conn, params) do
case Util.fetch_map_id(params) do
{:ok, map_identifier} ->
with {:ok, map} <- get_map(map_identifier),
# Load ACLs and each ACL's :owner in a single pass:
{:ok, loaded_map} <- Ash.load(map, acls: [:owner]) do
acls = loaded_map.acls || []
json(conn, %{data: Enum.map(acls, &acl_to_list_json/1)})
@@ -34,7 +255,7 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:error, :map_not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Map not found"})
|> json(%{error: "Map not found. Please provide a valid map_id or slug as a query parameter."})
{:error, error} ->
conn
@@ -42,10 +263,10 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|> json(%{error: inspect(error)})
end
{:error, msg} ->
{:error, _msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
|> json(%{error: "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"})
end
end
@@ -54,6 +275,50 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Creates a new ACL for a map.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :create,
summary: "Create a new ACL",
description: "Creates a new ACL for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: "00000000-0000-0000-0000-000000000000"
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
request_body: {"Access List parameters", "application/json", @acl_create_request_schema},
responses: [
ok: {"Access List", "application/json", @acl_create_response_schema},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
}
}}
]
def create(conn, params) do
with {:ok, map_identifier} <- Util.fetch_map_id(params),
{:ok, map} <- get_map(map_identifier),
@@ -71,6 +336,16 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:ok, _updated_map} <- associate_acl_with_map(map, new_acl) do
json(conn, %{data: acl_to_json(new_acl)})
else
{:error, :map_not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "Map not found. Please provide a valid map_id or slug as a query parameter."})
{:error, "Must provide either ?map_id=UUID or ?slug=SLUG"} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"})
nil ->
conn
|> put_status(:bad_request)
@@ -79,9 +354,13 @@ defmodule WandererAppWeb.MapAccessListAPIController do
{:error, "owner_eve_id does not match any existing character"} = error ->
conn
|> put_status(:bad_request)
|> json(%{error: inspect(error)})
|> json(%{error: "Character not found: The provided owner_eve_id does not match any existing character"})
%{} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Missing required 'acl' object in request body"})
# For any other error, also a bad request—adjust if you want a different code
error ->
conn
|> put_status(:bad_request)
@@ -94,6 +373,46 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Shows a specific ACL (with its members).
"""
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :show,
summary: "Get ACL details",
description: "Retrieves details for a specific ACL by its ID.",
parameters: [
id: [
in: :path,
description: "ACL identifier (UUID)",
type: :string,
required: true,
example: "00000000-0000-0000-0000-000000000000"
]
],
responses: [
ok: {
"ACL details",
"application/json",
@acl_show_response_schema
},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}},
internal_server_error: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to load ACL members: reason"
}
}}
]
def show(conn, %{"id" => id}) do
query =
AccessList
@@ -102,7 +421,6 @@ defmodule WandererAppWeb.MapAccessListAPIController do
case WandererApp.Api.read(query) do
{:ok, [acl]} ->
# We load members for a single ACL
case Ash.load(acl, :members) do
{:ok, loaded_acl} ->
json(conn, %{data: acl_to_json(loaded_acl)})
@@ -130,6 +448,51 @@ defmodule WandererAppWeb.MapAccessListAPIController do
Updates an ACL.
"""
@spec update(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :update,
summary: "Update an ACL",
description: "Updates an existing ACL by its ID.",
parameters: [
id: [
in: :path,
description: "ACL identifier (UUID)",
type: :string,
required: true,
example: "00000000-0000-0000-0000-000000000000"
]
],
request_body: {
"ACL update payload",
"application/json",
@acl_update_request_schema
},
responses: [
ok: {
"Updated ACL",
"application/json",
@acl_update_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Failed to update ACL: invalid parameters"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "ACL not found"
}
}}
]
def update(conn, %{"id" => id, "acl" => acl_params}) do
with {:ok, acl} <- AccessList.by_id(id),
{:ok, updated_acl} <- AccessList.update(acl, acl_params),
@@ -147,9 +510,10 @@ defmodule WandererAppWeb.MapAccessListAPIController do
# Private / Helper Functions
# ---------------------------------------------------------------------------
defp get_map(map_identifier) do
# If your WandererApp.Api.Map.by_id/1 returns :map_not_found or
# returns {:ok, map}/{:error, ...}, you can handle that here
WandererApp.Api.Map.by_id(map_identifier)
case WandererApp.Api.Map.by_id(map_identifier) do
{:ok, map} -> {:ok, map}
{:error, _} -> {:error, :map_not_found}
end
end
defp acl_to_json(acl) do
@@ -173,7 +537,6 @@ defmodule WandererAppWeb.MapAccessListAPIController do
end
defp acl_to_list_json(acl) do
# Because we loaded :owner for each ACL in index/2, we can reference it here
owner_eve_id =
case acl.owner do
%Character{eve_id: eid} -> eid
@@ -191,17 +554,22 @@ defmodule WandererAppWeb.MapAccessListAPIController do
end
defp member_to_json(member) do
%{
base = %{
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
}
cond do
member.eve_character_id -> Map.put(base, :eve_character_id, member.eve_character_id)
member.eve_corporation_id -> Map.put(base, :eve_corporation_id, member.eve_corporation_id)
member.eve_alliance_id -> Map.put(base, :eve_alliance_id, member.eve_alliance_id)
true -> base
end
end
# Helper to find a character by external EVE id.
defp find_character_by_eve_id(eve_id) do
query =
Character
@@ -225,8 +593,16 @@ defmodule WandererAppWeb.MapAccessListAPIController do
with {:ok, api_map} <- WandererApp.Api.Map.by_id(map.id),
{:ok, loaded_map} <- Ash.load(api_map, :acls) do
new_acl_id = if is_binary(new_acl), do: new_acl, else: new_acl.id
current_acls = loaded_map.acls || []
updated_acls = current_acls ++ [new_acl_id]
# Extract IDs from current ACLs to ensure we're working with UUIDs only
current_acl_ids = loaded_map.acls
|> Kernel.||([])
|> Enum.map(fn
acl when is_binary(acl) -> acl
acl -> acl.id
end)
updated_acls = current_acl_ids ++ [new_acl_id]
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
{:ok, updated_map} ->

View File

@@ -1,23 +1,132 @@
defmodule WandererAppWeb.AccessListMemberAPIController do
@moduledoc """
Handles creation, role updates, and deletion of individual ACL members.
This controller supports creation of members by accepting one of the following keys:
- "eve_character_id"
- "eve_corporation_id"
- "eve_alliance_id"
For corporation and alliance members, roles "admin" and "manager" are disallowed.
"""
use WandererAppWeb, :controller
use OpenApiSpex.ControllerSpecs
alias WandererApp.Api.AccessListMember
import Ash.Query
require Logger
# ------------------------------------------------------------------------
# Inline Schemas
# ------------------------------------------------------------------------
@acl_member_create_request_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
member: %OpenApiSpex.Schema{
type: :object,
properties: %{
eve_character_id: %OpenApiSpex.Schema{type: :string},
eve_corporation_id: %OpenApiSpex.Schema{type: :string},
eve_alliance_id: %OpenApiSpex.Schema{type: :string},
role: %OpenApiSpex.Schema{type: :string}
}
# no 'required' fields if you truly allow any of them
}
},
required: ["member"]
}
@acl_member_create_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
role: %OpenApiSpex.Schema{type: :string},
eve_character_id: %OpenApiSpex.Schema{type: :string},
eve_corporation_id: %OpenApiSpex.Schema{type: :string},
eve_alliance_id: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name", "role"]
}
},
required: ["data"]
}
@acl_member_update_request_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
member: %OpenApiSpex.Schema{
type: :object,
properties: %{
role: %OpenApiSpex.Schema{type: :string}
},
required: ["role"]
}
},
required: ["member"]
}
@acl_member_update_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
role: %OpenApiSpex.Schema{type: :string},
eve_character_id: %OpenApiSpex.Schema{type: :string},
eve_corporation_id: %OpenApiSpex.Schema{type: :string},
eve_alliance_id: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "name", "role"]
}
},
required: ["data"]
}
@acl_member_delete_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
ok: %OpenApiSpex.Schema{type: :boolean}
},
required: ["ok"]
}
# ------------------------------------------------------------------------
# ENDPOINTS
# ------------------------------------------------------------------------
@doc """
POST /api/acls/:acl_id/members
Creates a new ACL member.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :create,
summary: "Create ACL Member",
description: "Creates a new ACL member for a given ACL.",
parameters: [
acl_id: [
in: :path,
description: "Access List ID",
type: :string,
required: true
]
],
request_body: {
"ACL Member parameters",
"application/json",
@acl_member_create_request_schema
},
responses: [
ok: {
"Created ACL Member",
"application/json",
@acl_member_create_response_schema
}
]
def create(conn, %{"acl_id" => acl_id, "member" => member_params}) do
chosen =
cond do
@@ -44,7 +153,7 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
else
{key, type} = chosen
raw_id = Map.get(member_params, key)
id_str = to_string(raw_id) # handle string/integer input
id_str = to_string(raw_id)
role = Map.get(member_params, "role", "viewer")
if type in ["corporation", "alliance"] and role in ["admin", "manager"] do
@@ -93,13 +202,44 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
@doc """
PUT /api/acls/:acl_id/members/:member_id
Updates the role of an ACL member.
"""
@spec update_role(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :update_role,
summary: "Update ACL Member Role",
description: "Updates the role of an ACL member identified by ACL ID and member external ID.",
parameters: [
acl_id: [
in: :path,
description: "Access List ID",
type: :string,
required: true
],
member_id: [
in: :path,
description: "Member external ID",
type: :string,
required: true
]
],
request_body: {
"ACL Member update payload",
"application/json",
@acl_member_update_request_schema
},
responses: [
ok: {
"Updated ACL Member",
"application/json",
@acl_member_update_response_schema
}
]
def update_role(conn, %{
"acl_id" => acl_id,
"member_id" => external_id,
"member" => member_params
}) do
# Convert external_id to string if you expect it may come in as integer
external_id_str = to_string(external_id)
membership_query =
@@ -157,7 +297,34 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
@doc """
DELETE /api/acls/:acl_id/members/:member_id
Deletes an ACL member.
"""
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :delete,
summary: "Delete ACL Member",
description: "Deletes an ACL member identified by ACL ID and member external ID.",
parameters: [
acl_id: [
in: :path,
description: "Access List ID",
type: :string,
required: true
],
member_id: [
in: :path,
description: "Member external ID",
type: :string,
required: true
]
],
responses: [
ok: {
"ACL Member deletion confirmation",
"application/json",
@acl_member_delete_response_schema
}
]
def delete(conn, %{"acl_id" => acl_id, "member_id" => external_id}) do
external_id_str = to_string(external_id)
@@ -204,6 +371,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
id: member.id,
name: member.name,
role: member.role,
eve_character_id: member.eve_character_id,
eve_corporation_id: member.eve_corporation_id,
eve_alliance_id: member.eve_alliance_id,
inserted_at: member.inserted_at,
updated_at: member.updated_at
}

View File

@@ -1,20 +1,47 @@
defmodule WandererAppWeb.CharactersAPIController do
@moduledoc """
Exposes an endpoint for listing ALL characters in the database
Endpoint:
GET /api/characters
"""
use WandererAppWeb, :controller
use OpenApiSpex.ControllerSpecs
alias WandererApp.Api.Character
@characters_index_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
corporation_name: %OpenApiSpex.Schema{type: :string},
alliance_name: %OpenApiSpex.Schema{type: :string}
},
required: ["id", "eve_id", "name"]
}
}
},
required: ["data"]
}
@doc """
GET /api/characters
Lists ALL characters in the database
Returns an array of objects, each with `id`, `eve_id`, `name`, etc.
"""
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :index,
summary: "List Characters",
description: "Lists ALL characters in the database.",
responses: [
ok: {
"List of characters",
"application/json",
@characters_index_response_schema
}
]
def index(conn, _params) do
case WandererApp.Api.read(Character) do
{:ok, characters} ->

View File

@@ -1,17 +1,64 @@
defmodule WandererAppWeb.CommonAPIController do
use WandererAppWeb, :controller
use OpenApiSpex.ControllerSpecs
alias WandererApp.CachedInfo
alias WandererAppWeb.UtilAPIController, as: Util
@system_static_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :object,
properties: %{
solar_system_id: %OpenApiSpex.Schema{type: :integer},
region_id: %OpenApiSpex.Schema{type: :integer},
constellation_id: %OpenApiSpex.Schema{type: :integer},
solar_system_name: %OpenApiSpex.Schema{type: :string},
solar_system_name_lc: %OpenApiSpex.Schema{type: :string},
constellation_name: %OpenApiSpex.Schema{type: :string},
region_name: %OpenApiSpex.Schema{type: :string},
system_class: %OpenApiSpex.Schema{type: :integer},
security: %OpenApiSpex.Schema{type: :string},
type_description: %OpenApiSpex.Schema{type: :string},
class_title: %OpenApiSpex.Schema{type: :string},
is_shattered: %OpenApiSpex.Schema{type: :boolean},
effect_name: %OpenApiSpex.Schema{type: :string},
effect_power: %OpenApiSpex.Schema{type: :integer},
statics: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
wandering: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
triglavian_invasion_status: %OpenApiSpex.Schema{type: :string},
sun_type_id: %OpenApiSpex.Schema{type: :integer}
},
required: ["solar_system_id", "solar_system_name"]
}
},
required: ["data"]
}
@doc """
GET /api/common/system_static?id=<solar_system_id>
Requires 'id' (the solar_system_id).
Example:
GET /api/common/system_static?id=31002229
GET /api/common/system-static-info?id=<solar_system_id>
"""
@spec show_system_static(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :show_system_static,
summary: "Get System Static Information",
description: "Retrieves static information for a given solar system.",
parameters: [
id: [
in: :query,
description: "Solar system ID",
type: :string,
example: "30000142",
required: true
]
],
responses: [
ok: {
"System static info",
"application/json",
@system_static_response_schema
}
]
def show_system_static(conn, params) do
with {:ok, solar_system_str} <- Util.require_param(params, "id"),
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
@@ -33,10 +80,6 @@ defmodule WandererAppWeb.CommonAPIController do
end
end
# ----------------------------------------------
# Private helpers
# ----------------------------------------------
defp static_system_to_json(system) do
system
|> Map.take([

View File

@@ -1,11 +1,13 @@
defmodule WandererAppWeb.MapAPIController do
use WandererAppWeb, :controller
use OpenApiSpex.ControllerSpecs
import Ash.Query, only: [filter: 2]
require Logger
alias WandererApp.Api
alias WandererApp.Api.Character
alias WandererApp.Api.MapSolarSystem
alias WandererApp.MapSystemRepo
alias WandererApp.MapCharacterSettingsRepo
@@ -13,6 +15,166 @@ defmodule WandererAppWeb.MapAPIController do
alias WandererAppWeb.UtilAPIController, as: Util
# -----------------------------------------------------------------
# Inline Schemas
# -----------------------------------------------------------------
@map_system_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
map_id: %OpenApiSpex.Schema{type: :string},
solar_system_id: %OpenApiSpex.Schema{type: :integer},
original_name: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
custom_name: %OpenApiSpex.Schema{type: :string},
temporary_name: %OpenApiSpex.Schema{type: :string},
description: %OpenApiSpex.Schema{type: :string},
tag: %OpenApiSpex.Schema{type: :string},
labels: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
locked: %OpenApiSpex.Schema{type: :boolean},
visible: %OpenApiSpex.Schema{type: :boolean},
status: %OpenApiSpex.Schema{type: :string},
position_x: %OpenApiSpex.Schema{type: :integer},
position_y: %OpenApiSpex.Schema{type: :integer},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "solar_system_id", "original_name", "name"]
}
@list_map_systems_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @map_system_schema
}
},
required: ["data"]
}
@show_map_system_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: @map_system_schema
},
required: ["data"]
}
# For operation :tracked_characters_with_info
@character_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
corporation_id: %OpenApiSpex.Schema{type: :string},
corporation_name: %OpenApiSpex.Schema{type: :string},
corporation_ticker: %OpenApiSpex.Schema{type: :string},
alliance_id: %OpenApiSpex.Schema{type: :string},
alliance_name: %OpenApiSpex.Schema{type: :string},
alliance_ticker: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "eve_id", "name"]
}
@tracked_char_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
map_id: %OpenApiSpex.Schema{type: :string},
character_id: %OpenApiSpex.Schema{type: :string},
tracked: %OpenApiSpex.Schema{type: :boolean},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
character: @character_schema
},
required: ["id", "map_id", "character_id", "tracked"]
}
@tracked_characters_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @tracked_char_schema
}
},
required: ["data"]
}
# For operation :show_structure_timers
@structure_timer_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
system_id: %OpenApiSpex.Schema{type: :string},
solar_system_name: %OpenApiSpex.Schema{type: :string},
solar_system_id: %OpenApiSpex.Schema{type: :integer},
structure_type_id: %OpenApiSpex.Schema{type: :integer},
structure_type: %OpenApiSpex.Schema{type: :string},
character_eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
notes: %OpenApiSpex.Schema{type: :string},
owner_name: %OpenApiSpex.Schema{type: :string},
owner_ticker: %OpenApiSpex.Schema{type: :string},
owner_id: %OpenApiSpex.Schema{type: :string},
status: %OpenApiSpex.Schema{type: :string},
end_time: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["system_id", "solar_system_id", "name", "status"]
}
@structure_timers_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @structure_timer_schema
}
},
required: ["data"]
}
# For operation :list_systems_kills
@kill_item_schema %OpenApiSpex.Schema{
type: :object,
description: "Kill detail object",
properties: %{
kill_id: %OpenApiSpex.Schema{type: :integer, description: "Unique identifier for the kill"},
kill_time: %OpenApiSpex.Schema{type: :string, format: :date_time, description: "Time when the kill occurred"},
victim_id: %OpenApiSpex.Schema{type: :integer, description: "ID of the victim character"},
victim_name: %OpenApiSpex.Schema{type: :string, description: "Name of the victim character"},
ship_type_id: %OpenApiSpex.Schema{type: :integer, description: "Type ID of the destroyed ship"},
ship_name: %OpenApiSpex.Schema{type: :string, description: "Name of the destroyed ship"}
}
}
@system_kills_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
solar_system_id: %OpenApiSpex.Schema{type: :integer},
kills: %OpenApiSpex.Schema{
type: :array,
items: @kill_item_schema
}
},
required: ["solar_system_id", "kills"]
}
@systems_kills_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @system_kills_schema
}
},
required: ["data"]
}
# -----------------------------------------------------------------
# MAP endpoints
# -----------------------------------------------------------------
@@ -28,6 +190,43 @@ defmodule WandererAppWeb.MapAPIController do
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/systems?slug=my-unique-wormhole-map
"""
@spec list_systems(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :list_systems,
summary: "List Map Systems",
description: "Lists all visible systems for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
responses: [
ok: {
"List of map systems",
"application/json",
@list_map_systems_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG"
}
}}
]
def list_systems(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params),
{:ok, systems} <- MapSystemRepo.get_visible_by_map(map_id) do
@@ -56,6 +255,60 @@ defmodule WandererAppWeb.MapAPIController do
GET /api/map/system?id=31002229&map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/system?id=31002229&slug=my-unique-wormhole-map
"""
@spec show_system(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :show_system,
summary: "Show Map System",
description: "Retrieves details for a specific map system (by solar_system_id + map). Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
id: [
in: :query,
description: "System ID",
type: :string,
required: true,
example: "30000142"
],
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
responses: [
ok: {
"Map system details",
"application/json",
@show_map_system_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "System not found"
}
}}
]
def show_system(conn, params) do
with {:ok, solar_system_str} <- Util.require_param(params, "id"),
{:ok, solar_system_id} <- Util.parse_int(solar_system_str),
@@ -90,6 +343,43 @@ defmodule WandererAppWeb.MapAPIController do
Returns a list of tracked records, plus their fully-loaded `character` data.
"""
@spec tracked_characters_with_info(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :tracked_characters_with_info,
summary: "List Tracked Characters with Info",
description: "Lists all tracked characters for a map with their information. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
responses: [
ok: {
"List of tracked characters",
"application/json",
@tracked_characters_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG"
}
}}
]
def tracked_characters_with_info(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params),
{:ok, settings_list} <- get_tracked_by_map_ids(map_id),
@@ -151,6 +441,50 @@ defmodule WandererAppWeb.MapAPIController do
GET /api/map/structure_timers?map_id=<uuid>&system_id=31002229
```
"""
@spec show_structure_timers(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :show_structure_timers,
summary: "Show Structure Timers",
description: "Retrieves structure timers for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
],
system_id: [
in: :query,
description: "System ID",
type: :string,
required: false,
example: "30000142"
]
],
responses: [
ok: {
"Structure timers",
"application/json",
@structure_timers_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}}
]
def show_structure_timers(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params) do
system_id_str = params["system_id"]
@@ -191,6 +525,50 @@ defmodule WandererAppWeb.MapAPIController do
GET /api/map/systems_kills?slug=<map-slug>
GET /api/map/systems_kills?map_id=<uuid>&hours_ago=<somehours>
"""
@spec list_systems_kills(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :list_systems_kills,
summary: "List Systems Kills",
description: "Returns kills data for all visible systems on the map, optionally filtered by hours_ago. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
],
hours: [
in: :query,
description: "Number of hours to look back for kills",
type: :string,
required: false,
example: "24"
]
],
responses: [
ok: {
"Systems kills data",
"application/json",
@systems_kills_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}}
]
def list_systems_kills(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params),
# fetch visible systems from the repo
@@ -245,15 +623,6 @@ defmodule WandererAppWeb.MapAPIController do
end
end
@doc """
GET /api/map/systems-kills
This is an alias for list_systems_kills to support the hyphenated URL format.
See list_systems_kills for full documentation.
"""
def list_systems_kills_hyphenated(conn, params) do
list_systems_kills(conn, params)
end
# If hours_str is present and valid, parse it. Otherwise return nil (no filter).
defp parse_hours_ago(nil), do: nil
@@ -409,11 +778,14 @@ defmodule WandererAppWeb.MapAPIController do
end
defp map_system_to_json(system) do
Map.take(system, [
# Get the original system name from the database
original_name = get_original_system_name(system.solar_system_id)
# Start with the basic system data
result = Map.take(system, [
:id,
:map_id,
:solar_system_id,
:name,
:custom_name,
:temporary_name,
:description,
@@ -427,6 +799,35 @@ defmodule WandererAppWeb.MapAPIController do
:inserted_at,
:updated_at
])
# Add the original name
result = Map.put(result, :original_name, original_name)
# Set the name field based on the display priority:
# 1. If temporary_name is set, use that
# 2. If custom_name is set, use that
# 3. Otherwise, use the original system name
display_name = cond do
not is_nil(system.temporary_name) and system.temporary_name != "" ->
system.temporary_name
not is_nil(system.custom_name) and system.custom_name != "" ->
system.custom_name
true ->
original_name
end
# Add the display name as the "name" field
Map.put(result, :name, display_name)
end
defp get_original_system_name(solar_system_id) do
# Fetch the original system name from the MapSolarSystem resource
case WandererApp.Api.MapSolarSystem.by_solar_system_id(solar_system_id) do
{:ok, system} ->
system.solar_system_name
_error ->
"Unknown System"
end
end
defp character_to_json(ch) do

View File

@@ -1,7 +1,7 @@
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
@moduledoc """
A plug that checks the "Authorization: Bearer <token>" header
against the maps stored public_api_key. Halts with 401 if invalid.
against the map's stored public_api_key. Halts with 401 if invalid.
"""
import Plug.Conn
@@ -20,19 +20,22 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
conn
else
conn
|> send_resp(401, "Unauthorized (invalid token for map)")
|> put_resp_content_type("application/json")
|> send_resp(401, Jason.encode!(%{error: "Unauthorized (invalid token for map)"}))
|> halt()
end
{:error, _reason} ->
conn
|> send_resp(404, "Map not found")
|> put_resp_content_type("application/json")
|> send_resp(404, Jason.encode!(%{error: "Map not found"}))
|> halt()
end
_ ->
conn
|> send_resp(401, "Missing or invalid 'Bearer' token")
|> put_resp_content_type("application/json")
|> send_resp(401, Jason.encode!(%{error: "Missing or invalid 'Bearer' token"}))
|> halt()
end
end

View File

@@ -18,23 +18,78 @@ defmodule WandererAppWeb.Router do
[WandererAppWeb.Endpoint, :code_reloader],
false
)
@frame_src if(@code_reloading, do: ~w('self'), else: ~w())
@style_src ~w('self' 'unsafe-inline' https://fonts.googleapis.com)
@img_src ~w('self' data: https://images.evetech.net https://web.ccpgamescdn.com https://images.ctfassets.net https://w.appzi.io)
@font_src ~w('self' https://fonts.gstatic.com data: https://web.ccpgamescdn.com https://w.appzi.io )
@script_src ~w('self' )
@frame_src_values if(@code_reloading, do: ["'self'"], else: [])
# Define style sources individually to ensure proper spacing
@style_src_values [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com",
"https://cdn.jsdelivr.net/npm/",
"https://cdnjs.cloudflare.com/ajax/libs/"
]
# Define image sources individually to ensure proper spacing
@img_src_values [
"'self'",
"data:",
"https://images.evetech.net",
"https://web.ccpgamescdn.com",
"https://images.ctfassets.net",
"https://w.appzi.io"
]
# Define font sources individually to ensure proper spacing
@font_src_values [
"'self'",
"https://fonts.gstatic.com",
"data:",
"https://web.ccpgamescdn.com",
"https://w.appzi.io"
]
# Define script sources individually to ensure proper spacing
@script_src_values [
"'self'",
"'unsafe-inline'",
"https://cdn.jsdelivr.net/npm/",
"https://cdnjs.cloudflare.com/ajax/libs/",
"https://unpkg.com",
"https://cdn.jsdelivr.net",
"https://w.appzi.io",
"https://www.googletagmanager.com",
"https://cdnjs.cloudflare.com"
]
# Define connect sources individually to ensure proper spacing
@connect_src_values [
"'self'",
"https://api.appzi.io",
"https://www.googletagmanager.com",
"https://www.google-analytics.com"
]
# Define sandbox values individually to ensure proper spacing
@sandbox_values [
"allow-forms",
"allow-scripts",
"allow-modals",
"allow-same-origin",
"allow-downloads",
"allow-popups"
]
pipeline :admin_bauth do
plug :admin_basic_auth
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, html: {WandererAppWeb.Layouts, :root})
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {WandererAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
dynamic_plug PlugContentSecurityPolicy, reevaluate: :first_usage do
URI.default_port("wss", 443)
@@ -51,41 +106,37 @@ defmodule WandererAppWeb.Router do
|> Map.put(:path, "")
|> URI.to_string()
# Get the HTTP URL from home_url
http_url = URI.to_string(home_url)
# Only add script-src-elem when in development mode
script_src_elem = if(@code_reloading, do:
@script_src_values ++ [ws_url, http_url],
else: @script_src_values)
directives = %{
default_src: ~w('none'),
script_src: [
@script_src,
~w('unsafe-inline'),
~w(https://unpkg.com),
~w(https://cdn.jsdelivr.net),
~w(https://w.appzi.io),
~w(https://www.googletagmanager.com),
~w(https://cdnjs.cloudflare.com)
],
style_src: @style_src,
img_src: @img_src,
font_src: @font_src,
connect_src: [
ws_url,
~w('self'),
~w(https://api.appzi.io),
~w(https://www.googletagmanager.com),
~w(https://www.google-analytics.com)
],
script_src: @script_src_values ++ [ws_url],
style_src: @style_src_values,
img_src: @img_src_values,
font_src: @font_src_values,
connect_src: @connect_src_values ++ [ws_url],
media_src: ~w('none'),
object_src: ~w('none'),
child_src: ~w('none'),
frame_src: [@frame_src],
frame_src: @frame_src_values,
worker_src: ~w('none'),
frame_ancestors: ~w('none'),
form_action: ~w('self'),
block_all_mixed_content: ~w(),
sandbox:
~w(allow-forms allow-scripts allow-modals allow-same-origin allow-downloads allow-popups),
sandbox: @sandbox_values,
base_uri: ~w('none'),
manifest_src: ~w('self')
}
# Only add script-src-elem to directives when in development mode
directives = Map.put(directives, :script_src_elem, script_src_elem)
directives =
case home_url do
%URI{scheme: "http"} -> directives
@@ -101,11 +152,11 @@ defmodule WandererAppWeb.Router do
end
pipeline :blog do
plug(:put_layout, html: {WandererAppWeb.Layouts, :blog})
plug :put_layout, html: {WandererAppWeb.Layouts, :blog}
end
pipeline :api do
plug(:accepts, ["json"])
plug :accepts, ["json"]
plug WandererAppWeb.Plugs.CheckApiDisabled
end
@@ -126,6 +177,12 @@ defmodule WandererAppWeb.Router do
plug WandererAppWeb.Plugs.CheckAclApiKey
end
pipeline :api_spec do
plug OpenApiSpex.Plug.PutApiSpec,
otp_app: :wanderer_app,
module: WandererAppWeb.ApiSpec
end
scope "/api/map/systems-kills", WandererAppWeb do
pipe_through [:api, :api_map, :api_kills]
@@ -162,6 +219,11 @@ defmodule WandererAppWeb.Router do
get "/system-static-info", CommonAPIController, :show_system_static
end
scope "/api" do
pipe_through [:browser, :api, :api_spec]
get "/openapi", OpenApiSpex.Plug.RenderSpec, :show
end
#
# Browser / blog stuff
#
@@ -191,6 +253,30 @@ defmodule WandererAppWeb.Router do
get "/", BlogController, :license
end
scope "/swaggerui" do
pipe_through [:browser, :api, :api_spec]
get "/", OpenApiSpex.Plug.SwaggerUI,
path: "/api/openapi",
title: "WandererApp API Docs",
css_urls: [
# Standard Swagger UI CSS
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui.min.css",
# Material theme from swagger-ui-themes (v3.x):
"https://cdn.jsdelivr.net/npm/swagger-ui-themes@3.0.0/themes/3.x/theme-material.css"
],
js_urls: [
# We need both main JS & standalone preset for full styling
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-standalone-preset.min.js"
],
favicon_url: "https://example.com/my_favicon.ico",
swagger_ui_config: %{
"docExpansion" => "none",
"deepLinking" => true
}
end
#
# Auth
#

View File

@@ -54,6 +54,7 @@ defmodule WandererApp.MixProject do
{:sobelow, ">= 0.0.0", only: [:dev], runtime: false},
{:mix_audit, ">= 0.0.0", only: [:dev], runtime: false},
{:ex_check, "~> 0.14.0", only: [:dev], runtime: false},
{:open_api_spex, github: "mbuhot/open_api_spex", branch: "master"},
{:ex_rated, "~> 2.0"},
{:retry, "~> 0.18.0"},
{:phoenix, "~> 1.7.12"},

View File

@@ -78,6 +78,7 @@
"nimble_publisher": {:hex, :nimble_publisher, "1.1.0", "49dee0f30536140268996660a5927d0282946949c35c88ccc6da11a19231b4b6", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "80fb42d8d1e34f41ff29fc2a1ae6ab86ea7b764b3c2d38e5268a43cf33825782"},
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
"open_api_spex": {:git, "https://github.com/mbuhot/open_api_spex.git", "abe90e3db0cab2e75ede364ee24f26c9e490f74f", [branch: "master"]},
"owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"},
"parent": {:hex, :parent, "0.12.1", "495c4386f06de0df492e0a7a7199c10323a55e9e933b27222060dd86dccd6d62", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2ab589ef1f37bfcedbfb5ecfbab93354972fb7391201b8907a866dadd20b39d1"},
"pathex": {:hex, :pathex, "2.5.3", "0f2674c7cb52ae9220766cae2653b4013578349ae5ec07cb0c31b92684b3f19a", [:mix], [], "hexpm", "767aefc27d0303f583ba2064f0a49546067ab5de3c42b89f014a0ba32ea04830"},
@@ -103,6 +104,7 @@
"quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"reactor": {:hex, :reactor, "0.10.0", "1206113c21ba69b889e072b2c189c05a7aced523b9c3cb8dbe2dab7062cb699a", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4003c33e4c8b10b38897badea395e404d74d59a31beb30469a220f2b1ffe6457"},
"redoc_ui_plug": {:hex, :redoc_ui_plug, "0.2.1", "5e9760c17ed450fc9df671d5fbc70a6f06179c41d9d04ae3c33f16baca3a5b19", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7be01db31f210887e9fc18f8fbccc7788de32c482b204623556e415ed1fe714b"},
"req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"},
"retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"},
"rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},

View File

@@ -10,7 +10,7 @@
## Introduction
Wanderers 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:
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.
@@ -30,8 +30,8 @@ Unless otherwise noted, these endpoints require a valid **Bearer** token. Pass i
Authorization: Bearer <REDACTED_TOKEN>
```
If the token is missing or invalid, youll receive a `401 Unauthorized` error.
_(No API key is required for some common endpoints, but ACL- and character-related endpoints require a valid 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:
@@ -152,17 +152,35 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"members": [
{
"id": "8d63ab1e-b44f-4e81-8227-8fb8d928dad8",
"name": "Other Character",
"name": "Character Name",
"role": "admin",
"eve_character_id": "2122019111",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
},
...
{
"id": "7e52ab1e-c33f-5e81-9338-7fb8d928ebc9",
"name": "Corporation Name",
"role": "viewer",
"eve_corporation_id": "98140648",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
},
{
"id": "6f41bc2f-d44e-6f92-8449-8ec9e039fad7",
"name": "Alliance Name",
"role": "viewer",
"eve_alliance_id": "99013806",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
}
]
}
}
```
**Note:** The response for each member will include only one of `eve_character_id`, `eve_corporation_id`, or `eve_alliance_id` depending on the type of member.
---
### 4. Create a New ACL Associated with a Map
@@ -295,14 +313,13 @@ POST /api/acls/:acl_id/members
```json
{
"member": {
"name": "New Member",
"eve_character_id": "EXTERNAL_EVE_ID",
"role": "viewer"
}
}
```
- **Example Request:**
- **Example Request for Character:**
```bash
curl -X POST \
@@ -310,7 +327,6 @@ curl -X POST \
-H "Content-Type: application/json" \
-d '{
"member": {
"name": "New Member",
"eve_character_id": "EXTERNAL_EVE_ID",
"role": "viewer"
}
@@ -318,14 +334,45 @@ curl -X POST \
"https://wanderer.example.com/api/acls/ACL_UUID/members"
```
- **Example Response (redacted):**
- **Example Request for Corporation:**
```bash
curl -X POST \
-H "Authorization: Bearer <ACL_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"member": {
"eve_corporation_id": "CORPORATION_ID",
"role": "viewer"
}
}' \
"https://wanderer.example.com/api/acls/ACL_UUID/members"
```
- **Example Response for Character (redacted):**
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "New Member",
"name": "Character Name",
"role": "viewer",
"eve_character_id": "EXTERNAL_EVE_ID",
"inserted_at": "...",
"updated_at": "..."
}
}
```
- **Example Response for Corporation (redacted):**
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "Corporation Name",
"role": "viewer",
"eve_corporation_id": "CORPORATION_ID",
"inserted_at": "...",
"updated_at": "..."
}
@@ -334,13 +381,13 @@ curl -X POST \
---
### 7. Change a Members Role
### 7. Change a Member's Role
```bash
PUT /api/acls/:acl_id/members/:member_id
```
- **Description:** Updates an ACL members role (e.g. from `viewer` to `admin`).
- **Description:** Updates an ACL member's role (e.g. from `viewer` to `admin`).
The `:member_id` is the external EVE id (or corp/alliance id) used when creating the membership.
- **Authentication:** Requires the ACL API Token.
- **Request Body Example:**
@@ -373,13 +420,17 @@ curl -X PUT \
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "New Member",
"name": "Character Name",
"role": "admin",
...
"eve_character_id": "EXTERNAL_EVE_ID",
"inserted_at": "...",
"updated_at": "..."
}
}
```
**Note:** The response will include only one of `eve_character_id`, `eve_corporation_id`, or `eve_alliance_id` depending on the type of member.
---
### 8. Remove a Member from an ACL
@@ -416,7 +467,7 @@ This guide outlines how to:
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 members role (`PUT /api/acls/:acl_id/members/:member_id`).
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.

View File

@@ -0,0 +1,837 @@
%{
title: "Comprehensive Guide: Wanderer API Documentation",
author: "Wanderer Team",
cover_image_uri: "/images/news/03-05-api/swagger-ui.png",
tags: ~w(api map acl characters documentation swagger),
description: "Complete documentation for Wanderer's public APIs, including map data, character information, and access control management. Includes interactive API documentation with Swagger UI."
}
---
# Comprehensive Guide to Wanderer's API
## Introduction
Wanderer provides a comprehensive set of public APIs that allow you to programmatically interact with the platform. This guide consolidates all available API endpoints, authentication methods, and includes interactive documentation options.
With these APIs, you can:
- Retrieve map data, including systems and their properties
- Access system static information
- Track character locations and activities
- Monitor kill activity in systems
- Manage Access Control Lists (ACLs) for permissions
- Add, update, and remove ACL members
This guide provides step-by-step instructions, request/response examples, and details on how to authenticate each call.
---
## Interactive API Documentation
For a more interactive experience, Wanderer provides a way to explore the API:
### Swagger UI
Access our Swagger UI documentation at:
```
/swaggerui
```
This interactive interface allows you to:
- Browse all available endpoints
- See request parameters and response schemas
- Test API calls directly from your browser
- View authentication requirements
![Swagger UI](/images/news/03-04-api/swagger-ui.png "Swagger UI Documentation")
---
## Authentication
Wanderer uses Bearer token authentication for API access. 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.
![Generate Map API Key](/images/news/01-05-map-public-api/generate-key.png "Generate Map API Key")
2. **ACL API Token:** Available in the create/edit ACL screen. This token is used for ACL member management endpoints.
![Generate ACL API Key](/images/news/02-20-acl-api/generate-key.png "Generate ACL API Key")
Pass the appropriate token in the `Authorization` header:
```bash
Authorization: Bearer <YOUR_TOKEN>
```
If the token is missing or invalid, you'll receive a `401 Unauthorized` error.
**Note:** Some "common" endpoints (like system static information) don't require authentication.
---
## Map Data Endpoints
### 1. List Systems
```bash
GET /api/map/systems?map_id=<UUID>
GET /api/map/systems?slug=<map-slug>
```
- **Description:** Retrieves a list of systems associated with the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
- `all=true` (optional) — if set, returns _all_ systems instead of only "visible" systems.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/systems?slug=some-slug"
```
#### Example Response
```json
{
"data": [
{
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-01T13:38:42.875843Z",
"updated_at": "2025-01-01T13:40:16.750234Z",
"locked": false,
"solar_system_id": "<REDACTED_NUMBER>",
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1125,
"position_y": -285
},
...
]
}
```
### 2. Show Single System
```bash
GET /api/map/system?id=<SOLAR_SYSTEM_ID>&map_id=<UUID>
GET /api/map/system?id=<SOLAR_SYSTEM_ID>&slug=<map-slug>
```
- **Description:** Retrieves information for a specific system on the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `id` (required) — the `solar_system_id`.
- Either `map_id` or `slug` (required).
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/system?id=<REDACTED_NUMBER>&slug=<REDACTED_SLUG>"
```
#### Example Response
```json
{
"data": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-03T06:30:02.069090Z",
"updated_at": "2025-01-03T07:47:07.471051Z",
"locked": false,
"solar_system_id": "<REDACTED_NUMBER>",
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1005,
"position_y": 765
}
}
```
### 3. System Static Information
```bash
GET /api/common/system-static-info?id=<SOLAR_SYSTEM_ID>
```
- **Description:** Retrieves the static information for a specific system.
- **Authentication:** No authentication required.
- **Parameters:**
- `id` (required) — the `solar_system_id`.
#### Example Request
```bash
curl "https://wanderer.example.com/api/common/system-static-info?id=31002229"
```
#### Example Response
```json
{
"data": {
"solar_system_id": 31002229,
"triglavian_invasion_status": "Normal",
"solar_system_name": "J132946",
"system_class": 5,
"region_id": 11000028,
"constellation_id": 21000278,
"solar_system_name_lc": "j132946",
"constellation_name": "E-C00278",
"region_name": "E-R00028",
"security": "-1.0",
"type_description": "Class 5",
"class_title": "C5",
"is_shattered": false,
"effect_name": null,
"effect_power": 5,
"statics": [
"H296"
],
"wandering": [
"D792",
"C140",
"Z142"
],
"sun_type_id": 38
}
}
```
### 4. List Tracked Characters
```bash
GET /api/map/characters?map_id=<UUID>
GET /api/map/characters?slug=<map-slug>
```
- **Description:** Retrieves a list of tracked characters for the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/characters?slug=some-slug"
```
#### Example Response
```json
{
"data": [
{
"id": "<REDACTED_ID>",
"character": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"inserted_at": "2025-01-01T05:24:18.461721Z",
"updated_at": "2025-01-03T07:45:52.294052Z",
"alliance_id": "<REDACTED>",
"alliance_name": "<REDACTED>",
"alliance_ticker": "<REDACTED>",
"corporation_id": "<REDACTED>",
"corporation_name": "<REDACTED>",
"corporation_ticker": "<REDACTED>",
"eve_id": "<REDACTED>"
},
"tracked": true,
"map_id": "<REDACTED_ID>"
},
...
]
}
```
### 5. Kills Activity
```bash
GET /api/map/systems-kills?map_id=<UUID>
GET /api/map/systems-kills?slug=<map-slug>
```
- **Description:** Retrieves the kill activity for the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/systems-kills?slug=some-slug"
```
#### Example Response
```json
{
"data": [
{
"kills": [
{
"attacker_count": 1,
"final_blow_alliance_id": 99013806,
"final_blow_alliance_ticker": "TCE",
"final_blow_char_id": 2116802670,
"final_blow_char_name": "Bambi Bunny",
"final_blow_corp_id": 98140648,
"final_blow_corp_ticker": "GNK3D",
"final_blow_ship_name": "Thrasher",
"final_blow_ship_type_id": 16242,
"kill_time": "2025-01-21T21:00:59Z",
"killmail_id": 124181782,
"npc": false,
"solar_system_id": 30002768,
"total_value": 10000,
"victim_alliance_id": null,
"victim_char_id": 2121725410,
"victim_char_name": "Bill Drummond",
"victim_corp_id": 98753095,
"victim_corp_ticker": "KSTJK",
"victim_ship_name": "Capsule",
"victim_ship_type_id": 670,
"zkb": {
"awox": false,
"destroyedValue": 10000,
"droppedValue": 0,
"fittedValue": 10000,
"hash": "777148f8bf344bade68a6a0821bfe0a37491a7a6",
"labels": ["cat:6","#:1","pvp","loc:highsec"],
"locationID": 50014064,
"npc": false,
"points": 1,
"solo": false,
"totalValue": 10000
}
},
...
],
"solar_system_id": 30002768
},
...
]
}
```
### 6. Structure Timers
```bash
GET /api/map/structure-timers?map_id=<UUID>
GET /api/map/structure-timers?slug=<map-slug>
```
- **Description:** Retrieves structure timers for the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
---
## Character and ACL Endpoints
### 1. List All Characters
```bash
GET /api/characters
```
- **Description:** Returns a list of all characters known to Wanderer.
- **Authentication:** Requires a valid API token.
- **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
```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 Map
```bash
GET /api/map/acls?map_id=<UUID>
GET /api/map/acls?slug=<map-slug>
```
- **Description:** Lists all ACLs associated with a map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/acls?slug=mapname"
```
#### Example Response
```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
```bash
GET /api/acls/:id
```
- **Description:** Fetches a single ACL by ID, with its members preloaded.
- **Authentication:** Requires ACL API Token.
- **Parameters:**
- `id` (required) — the ACL ID.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/acls/19712899-ec3a-47b1-b73b-2bae221c5513"
```
#### Example Response
```json
{
"data": {
"id": "19712899-ec3a-47b1-b73b-2bae221c5513",
"name": "aclName",
"description": null,
"owner_id": "d43a9083-2705-40c9-a314-f7f412346661",
"api_key": "REDACTED_API_KEY",
"inserted_at": "2025-02-13T03:32:25.144403Z",
"updated_at": "2025-02-13T03:32:25.144403Z",
"members": [
{
"id": "8d63ab1e-b44f-4e81-8227-8fb8d928dad8",
"name": "Character Name",
"role": "admin",
"eve_character_id": "2122019111",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
},
{
"id": "7e52ab1e-c33f-5e81-9338-7fb8d928ebc9",
"name": "Corporation Name",
"role": "viewer",
"eve_corporation_id": "98140648",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
},
{
"id": "6f41bc2f-d44e-6f92-8449-8ec9e039fad7",
"name": "Alliance Name",
"role": "viewer",
"eve_alliance_id": "99013806",
"inserted_at": "2025-02-13T03:33:32.332598Z",
"updated_at": "2025-02-13T03:33:36.644520Z"
}
]
}
}
```
**Note:** The response for each member will include only one of `eve_character_id`, `eve_corporation_id`, or `eve_alliance_id` depending on the type of member.
### 4. Create a New ACL
```bash
POST /api/map/acls
```
- **Description:** Creates a new ACL for a map and generates a new ACL API key.
- **Authentication:** Requires Map API Token.
- **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
```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 Response
```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).
- **Authentication:** Requires ACL API Token.
- **Parameters:**
- `id` (required) — the ACL ID.
- **Request Body Example:**
```json
{
"acl": {
"name": "Updated ACL Name",
"description": "This is the updated description"
}
}
```
#### 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"
}
}' \
"https://wanderer.example.com/api/acls/ACL_UUID"
```
#### Example Response
```json
{
"data": {
"id": "ACL_UUID",
"name": "Updated ACL Name",
"description": "This is the updated description",
"owner_id": "OWNER_ID",
"api_key": "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 ACL API Token.
- **Parameters:**
- `acl_id` (required) — the ACL ID.
- **Request Body Example:**
For **character** membership:
```json
{
"member": {
"eve_character_id": "EXTERNAL_EVE_ID",
"role": "viewer"
}
}
```
For **corporation** membership:
```json
{
"member": {
"eve_corporation_id": "CORPORATION_ID",
"role": "viewer"
}
}
```
For **alliance** membership:
```json
{
"member": {
"eve_alliance_id": "ALLIANCE_ID",
"role": "viewer"
}
}
```
#### Example Request for Character
```bash
curl -X POST \
-H "Authorization: Bearer <ACL_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"member": {
"eve_character_id": "EXTERNAL_EVE_ID",
"role": "viewer"
}
}' \
"https://wanderer.example.com/api/acls/ACL_UUID/members"
```
#### Example Response for Character
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "Character Name",
"role": "viewer",
"eve_character_id": "EXTERNAL_EVE_ID",
"inserted_at": "2025-02-15T12:30:45.123456Z",
"updated_at": "2025-02-15T12:30:45.123456Z"
}
}
```
#### Example Request for Corporation
```bash
curl -X POST \
-H "Authorization: Bearer <ACL_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"member": {
"eve_corporation_id": "CORPORATION_ID",
"role": "viewer"
}
}' \
"https://wanderer.example.com/api/acls/ACL_UUID/members"
```
#### Example Response for Corporation
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "Corporation Name",
"role": "viewer",
"eve_corporation_id": "CORPORATION_ID",
"inserted_at": "2025-02-15T12:30:45.123456Z",
"updated_at": "2025-02-15T12:30:45.123456Z"
}
}
```
#### Example Request for Alliance
```bash
curl -X POST \
-H "Authorization: Bearer <ACL_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"member": {
"eve_alliance_id": "ALLIANCE_ID",
"role": "viewer"
}
}' \
"https://wanderer.example.com/api/acls/ACL_UUID/members"
```
#### Example Response for Alliance
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "Alliance Name",
"role": "viewer",
"eve_alliance_id": "ALLIANCE_ID",
"inserted_at": "2025-02-15T12:30:45.123456Z",
"updated_at": "2025-02-15T12:30:45.123456Z"
}
}
```
**Note:** The response will include only one of `eve_character_id`, `eve_corporation_id`, or `eve_alliance_id` depending on the type of member being added.
### 7. Change a Member's Role
```bash
PUT /api/acls/:acl_id/members/:member_id
```
- **Description:** Updates an ACL member's role.
- **Authentication:** Requires ACL API Token.
- **Parameters:**
- `acl_id` (required) — the ACL ID.
- `member_id` (required) — the external EVE id (or corp/alliance id) used when creating the membership.
- **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 for Character
```json
{
"data": {
"id": "MEMBERSHIP_UUID",
"name": "Character Name",
"role": "admin",
"eve_character_id": "EXTERNAL_EVE_ID",
"inserted_at": "2025-02-15T12:30:45.123456Z",
"updated_at": "2025-02-15T12:35:22.654321Z"
}
}
```
**Note:** The response will include only one of `eve_character_id`, `eve_corporation_id`, or `eve_alliance_id` depending on the type of member being updated.
### 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 (or corp/alliance id) from the ACL.
- **Authentication:** Requires ACL API Token.
- **Parameters:**
- `acl_id` (required) — the ACL ID.
- `member_id` (required) — the external EVE id (or corp/alliance id) used when creating the membership.
#### 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 provides a comprehensive overview of Wanderer's API capabilities. With these endpoints, you can:
1. **Explore the API** using interactive documentation at `/swaggerui`
2. **Retrieve map data** including systems, characters, and kill activity
3. **Access system information** with or without authentication
4. **Manage Access Control Lists (ACLs)** for permissions
5. **Add, update, and remove ACL members** with different roles
For the most up-to-date and interactive documentation, we recommend using the Swagger UI at `/swaggerui` which allows you to explore and test endpoints directly from your browser.
If you have any questions or need assistance with the API, please reach out to the Wanderer Team.
Fly safe,
**WANDERER TEAM**

View File

@@ -0,0 +1,13 @@
# Wanderer API Testing Tool Configuration
# Generated on Thu Mar 6 14:52:00 UTC 2025
# Base configuration
HOST="http://localhost:4444"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY=""
# Selected IDs
SELECTED_ACL_ID=""
SELECTED_SYSTEM_ID=""
CHARACTER_EVE_ID=""

View File

@@ -0,0 +1,13 @@
# Wanderer API Testing Tool Configuration
# Example configuration file - Copy to .api_test_config and modify as needed
# Base configuration
HOST="http://localhost:4000"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY="acl-api-key-here"
# Selected IDs
SELECTED_ACL_ID="123"
SELECTED_SYSTEM_ID="31002019"
CHARACTER_EVE_ID="456"

View File

@@ -0,0 +1,13 @@
# Wanderer API Testing Tool Configuration
# Generated on Thu Mar 6 18:44:20 UTC 2025
# Base configuration
HOST="http://localhost:4444"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY="116bd70e-2bbf-4a99-97ed-1869c09ab5bf"
# Selected IDs
SELECTED_ACL_ID="9c91d283-f49f-4f45-a21d-9bf53ce9d1fd"
SELECTED_SYSTEM_ID="30002768"
CHARACTER_EVE_ID="2115754172"

894
test/manual/auto_test_api.sh Executable file
View File

@@ -0,0 +1,894 @@
#!/bin/bash
#==============================================================================
# Wanderer API Automated Testing Tool
#
# This script tests various endpoints of the Wanderer API.
#
# Features:
# - Uses strict mode (set -euo pipefail) for robust error handling.
# - Contains a DEBUG mode for extra logging (set DEBUG=1 to enable).
# - Validates configuration including a reachability test for the HOST.
# - Outputs a summary in plain text and optionally as JSON.
# - Exits with a nonzero code if any test fails.
#
# Usage:
# ./auto_test_api.sh
#
#==============================================================================
set -euo pipefail
IFS=$'\n\t'
# Set DEBUG=1 to enable extra logging
DEBUG=0
# Set VERBOSE=1 to print raw JSON responses for every test (default 0)
VERBOSE=0
# Set VERBOSE_SUMMARY=1 to output a JSON summary at the end (default 0)
VERBOSE_SUMMARY=0
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Configuration file and default configuration
CONFIG_FILE=".auto_api_test_config"
HOST="http://localhost:4444" # Default host
MAP_SLUG=""
MAP_API_KEY=""
ACL_API_KEY=""
SELECTED_ACL_ID=""
SELECTED_SYSTEM_ID=""
CHARACTER_EVE_ID=""
TEST_RESULTS=()
FAILED_TESTS=()
# Global variables for last API response
LAST_JSON_RESPONSE=""
LAST_HTTP_CODE=""
#------------------------------------------------------------------------------
# Helper Functions
#------------------------------------------------------------------------------
debug() {
if [ "$DEBUG" -eq 1 ]; then
echo -e "${YELLOW}[DEBUG] $*${NC}" >&2
fi
}
print_header() {
echo -e "\n${BLUE}=== $1 ===${NC}\n"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}" >&2
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if the host is reachable; accept any HTTP status code 200-399.
check_host_reachable() {
debug "Checking if host $HOST is reachable..."
local status
status=$(curl -s -o /dev/null -w "%{http_code}" "$HOST")
debug "HTTP status code for host: $status"
if [[ "$status" -ge 200 && "$status" -lt 400 ]]; then
print_success "Host $HOST is reachable."
else
print_error "Host $HOST is not reachable (HTTP code: $status). Please check the host URL."
exit 1
fi
}
# Load configuration from file
load_config() {
if [ -f "$CONFIG_FILE" ]; then
print_success "Loading configuration from $CONFIG_FILE"
source "$CONFIG_FILE"
return 0
else
print_warning "No configuration file found. Using default values."
return 1
fi
}
# Save configuration to file
save_config() {
print_success "Saving configuration to $CONFIG_FILE"
cat > "$CONFIG_FILE" << EOF
# Wanderer API Testing Tool Configuration
# Generated on $(date)
# Base configuration
HOST="$HOST"
MAP_SLUG="$MAP_SLUG"
MAP_API_KEY="$MAP_API_KEY"
ACL_API_KEY="$ACL_API_KEY"
# Selected IDs
SELECTED_ACL_ID="$SELECTED_ACL_ID"
SELECTED_SYSTEM_ID="$SELECTED_SYSTEM_ID"
CHARACTER_EVE_ID="$CHARACTER_EVE_ID"
EOF
chmod 600 "$CONFIG_FILE"
print_success "Configuration saved successfully."
}
# Make an API call using curl and capture response and HTTP code
call_api() {
local method=$1
local endpoint=$2
local api_key=$3
local data=${4:-""}
local curl_cmd=(curl -s -w "\n%{http_code}" -X "$method" -H "Content-Type: application/json")
if [ -n "$api_key" ]; then
curl_cmd+=(-H "Authorization: Bearer $api_key")
fi
if [ -n "$data" ]; then
curl_cmd+=(-d "$data")
fi
curl_cmd+=("$HOST$endpoint")
# Print debug command (mask API key)
local debug_cmd
debug_cmd=$(printf "%q " "${curl_cmd[@]}")
debug_cmd=$(echo "$debug_cmd" | sed "s/$api_key/API_KEY_HIDDEN/g")
print_warning "Executing: $debug_cmd"
local output
output=$("${curl_cmd[@]}")
LAST_HTTP_CODE=$(echo "$output" | tail -n1)
local response
response=$(echo "$output" | sed '$d')
echo "$response"
}
# Check that required variables are set
check_required_vars() {
local missing=false
if [ $# -eq 0 ]; then
if [ -z "$HOST" ]; then
print_error "HOST is not set. Please set it first."
missing=true
fi
if [ -z "$MAP_SLUG" ]; then
print_error "MAP_SLUG is not set. Please set it first."
missing=true
fi
if [ -z "$MAP_API_KEY" ]; then
print_error "MAP_API_KEY is not set. Please set it first."
missing=true
fi
else
for var in "$@"; do
if [ -z "${!var}" ]; then
print_error "$var is not set. Please set it first."
missing=true
fi
done
fi
$missing && return 1 || return 0
}
# Record a test result
record_test_result() {
local endpoint=$1
local status=$2
local message=$3
if [ "$status" = "success" ]; then
TEST_RESULTS+=("${GREEN}${NC} $endpoint - $message")
else
TEST_RESULTS+=("${RED}${NC} $endpoint - $message")
FAILED_TESTS+=("$endpoint - $message")
fi
}
# Process and validate the JSON response
check_response() {
local response=$1
local endpoint=$2
if [ -z "$(echo "$response" | xargs)" ]; then
if [ "$LAST_HTTP_CODE" = "200" ] || [ "$LAST_HTTP_CODE" = "204" ]; then
print_success "Received empty response, which is valid"
LAST_JSON_RESPONSE="{}"
return 0
else
record_test_result "$endpoint" "failure" "Empty response with HTTP code $LAST_HTTP_CODE"
return 1
fi
fi
if [ "$VERBOSE" -eq 1 ]; then
echo "Raw response from $endpoint:"
echo "$response" | head -n 20
fi
if echo "$response" | jq . > /dev/null 2>&1; then
LAST_JSON_RESPONSE="$response"
return 0
fi
local json_part
json_part=$(echo "$response" | grep -o '{.*}' || echo "")
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | sed -n '/^{/,$p' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | sed -n '/{/,/}/p' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | awk '!(/^[<>*]/) {print}' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
echo "Raw response from $endpoint:"
echo "$response"
record_test_result "$endpoint" "failure" "Invalid JSON response"
return 1
fi
local error
error=$(echo "$json_part" | jq -r '.error // empty')
if [ -n "$error" ]; then
echo "Raw response from $endpoint:"
echo "$response"
echo "Parsed JSON response from $endpoint:"
echo "$json_part" | jq '.'
record_test_result "$endpoint" "failure" "Error: $error"
return 1
fi
LAST_JSON_RESPONSE="$json_part"
return 0
}
# Get a random item from a JSON array using a jq path
get_random_item() {
local json=$1
local jq_path=$2
local count
count=$(echo "$json" | jq "$jq_path | length")
if [ "$count" -eq 0 ]; then
echo ""
return 1
fi
local random_index=$((RANDOM % count))
echo "$json" | jq -r "$jq_path[$random_index]"
}
#------------------------------------------------------------------------------
# API Test Functions
#------------------------------------------------------------------------------
test_list_characters() {
print_header "Testing GET /api/characters"
print_success "Calling API: GET /api/characters"
local response
response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/characters"; then
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -gt 0 ]; then
record_test_result "GET /api/characters" "success" "Found $character_count characters"
if [ -z "$CHARACTER_EVE_ID" ]; then
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
return 0
else
record_test_result "GET /api/characters" "success" "No characters found"
return 0
fi
}
test_map_systems() {
print_header "Testing GET /api/map/systems"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/systems" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/systems?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/systems?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/systems"; then
return 1
fi
local system_count
system_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "System count: $system_count"
if [ "$system_count" -gt 0 ]; then
record_test_result "GET /api/map/systems" "success" "Found $system_count systems"
local random_index=$((RANDOM % system_count))
print_success "Selecting system at index $random_index"
echo "Data structure:"
echo "$LAST_JSON_RESPONSE" | jq '.data[0]'
local random_system
random_system=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
echo "Selected system JSON:"
echo "$random_system"
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.solar_system_id')
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.id // .system_id // empty')
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
print_error "Could not find system ID in the response"
echo "Available fields:"
echo "$random_system" | jq 'keys'
record_test_result "GET /api/map/systems" "failure" "Could not extract system ID"
return 1
fi
fi
local system_name
system_name=$(echo "$random_system" | jq -r '.name // "Unknown"')
print_success "Selected random system: $system_name (ID: $SELECTED_SYSTEM_ID)"
return 0
else
record_test_result "GET /api/map/systems" "failure" "No systems found"
return 1
fi
}
test_map_system() {
print_header "Testing GET /api/map/system"
if [[ -z "$MAP_SLUG" || -z "$SELECTED_SYSTEM_ID" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/system" "failure" "Missing required variables"
return
fi
local response
response=$(call_api "GET" "/api/map/system?slug=$MAP_SLUG&id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
print_warning "Response: $response"
local trimmed_response
trimmed_response=$(echo "$response" | xargs)
if [[ "$trimmed_response" == "{}" || "$trimmed_response" == '{"data":{}}' ]]; then
print_success "Received empty JSON response, which is valid"
record_test_result "GET /api/map/system" "success" "Received valid empty response"
return
fi
if ! check_response "$response" "GET /api/map/system"; then
return
fi
local json_data="$LAST_JSON_RESPONSE"
local has_data
has_data=$(echo "$json_data" | jq 'has("data")')
if [ "$has_data" != "true" ]; then
print_error "Response does not contain 'data' field"
echo "JSON Response:"
echo "$json_data" | jq .
record_test_result "GET /api/map/system" "failure" "Response does not contain 'data' field"
return
fi
local system_data
system_data=$(echo "$json_data" | jq -r '.data // empty')
if [ -z "$system_data" ] || [ "$system_data" = "null" ]; then
print_error "Could not find system data in response"
echo "JSON Response:"
echo "$json_data" | jq .
record_test_result "GET /api/map/system" "failure" "Could not find system data in response"
return
fi
local system_id
system_id=$(echo "$json_data" | jq -r '.data.solar_system_id // empty')
if [ -z "$system_id" ] || [ "$system_id" = "null" ]; then
print_error "Could not find solar_system_id in the system data"
echo "System Data:"
echo "$system_data" | jq .
record_test_result "GET /api/map/system" "failure" "Could not find solar_system_id in system data"
return
fi
print_success "Found system data with ID: $system_id"
record_test_result "GET /api/map/system" "success" "Found system data with ID: $system_id"
}
test_map_characters() {
print_header "Testing GET /api/map/characters"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/characters" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/characters?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/characters?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/characters"; then
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
record_test_result "GET /api/map/characters" "success" "Found $character_count tracked characters"
return 0
}
test_map_structure_timers() {
print_header "Testing GET /api/map/structure-timers"
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/structure-timers" "failure" "Missing required variables"
return
fi
local response
response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG" "$MAP_API_KEY")
local trimmed_response
trimmed_response=$(echo "$response" | xargs)
if [[ "$trimmed_response" == '{"data":[]}' ]]; then
print_success "Found 0 structure timers"
record_test_result "GET /api/map/structure-timers" "success" "Found 0 structure timers"
fi
if ! check_response "$response" "GET /api/map/structure-timers"; then
return
fi
local timer_count
timer_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found $timer_count structure timers"
record_test_result "GET /api/map/structure-timers" "success" "Found $timer_count structure timers"
if [ -n "$SELECTED_SYSTEM_ID" ]; then
print_header "Testing GET /api/map/structure-timers (filtered)"
local filtered_response
filtered_response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG&system_id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
print_warning "(Structure Timers) - Filtered response: $filtered_response"
local trimmed_filtered
trimmed_filtered=$(echo "$filtered_response" | xargs)
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
print_success "Found 0 filtered structure timers"
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found 0 filtered structure timers"
return
fi
if ! check_response "$filtered_response" "GET /api/map/structure-timers (filtered)"; then
return
fi
local filtered_count
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found $filtered_count filtered structure timers"
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found $filtered_count filtered structure timers"
fi
}
test_map_systems_kills() {
print_header "Testing GET /api/map/systems-kills"
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/systems-kills" "failure" "Missing required variables"
return
fi
# Use the correct parameter name: hours
local response
response=$(call_api "GET" "/api/map/systems-kills?slug=$MAP_SLUG&hours=1" "$MAP_API_KEY")
print_warning "(Systems Kills) - Response: $response"
if ! check_response "$response" "GET /api/map/systems-kills"; then
return
fi
local json_data="$LAST_JSON_RESPONSE"
if [ "$VERBOSE" -eq 1 ]; then
echo "JSON Response:"; echo "$json_data" | jq .
fi
local has_data
has_data=$(echo "$json_data" | jq 'has("data")')
if [ "$has_data" != "true" ]; then
print_error "Response does not contain 'data' field"
if [ "$VERBOSE" -eq 1 ]; then
echo "JSON Response:"; echo "$json_data" | jq .
fi
record_test_result "GET /api/map/systems-kills" "failure" "Response does not contain 'data' field"
return
fi
local systems_count
systems_count=$(echo "$json_data" | jq '.data | length')
print_success "Found kill data for $systems_count systems"
record_test_result "GET /api/map/systems-kills" "success" "Found kill data for $systems_count systems"
print_header "Testing GET /api/map/systems-kills (filtered)"
local filter_url="/api/map/systems-kills?slug=$MAP_SLUG&hours=1"
if [ -n "$SELECTED_SYSTEM_ID" ]; then
filter_url="$filter_url&system_id=$SELECTED_SYSTEM_ID"
print_success "Using system_id filter to reduce response size"
fi
local filtered_response
filtered_response=$(call_api "GET" "$filter_url" "$MAP_API_KEY")
local trimmed_filtered
trimmed_filtered=$(echo "$filtered_response" | xargs)
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
print_success "Found 0 filtered systems with kill data"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found 0 filtered systems with kill data"
return
fi
if [[ "$trimmed_filtered" == '{"data":'* ]]; then
print_success "Received valid JSON response (large data)"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Received valid JSON response with kill data"
return
fi
if ! check_response "$filtered_response" "GET /api/map/systems-kills (filtered)"; then
return
fi
local filtered_count
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found filtered kill data for $filtered_count systems"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found filtered kill data for $filtered_count systems"
}
test_map_acls() {
print_header "Testing GET /api/map/acls"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/acls" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/acls?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/acls"; then
return 1
fi
local acl_count
acl_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
record_test_result "GET /api/map/acls" "success" "Found $acl_count ACLs"
if [ "$acl_count" -gt 0 ]; then
local random_acl
random_acl=$(get_random_item "$LAST_JSON_RESPONSE" ".data")
SELECTED_ACL_ID=$(echo "$random_acl" | jq -r '.id')
local acl_name
acl_name=$(echo "$random_acl" | jq -r '.name')
print_success "Selected random ACL: $acl_name (ID: $SELECTED_ACL_ID)"
else
print_warning "No ACLs found to select for future tests"
fi
return 0
}
test_create_acl() {
print_header "Testing POST /api/map/acls"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "POST /api/map/acls" "failure" "Missing required variables"
return 1
fi
if [ -z "$CHARACTER_EVE_ID" ]; then
print_warning "No character EVE ID selected. Fetching characters..."
print_success "Calling API: GET /api/characters"
local characters_response
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$characters_response" "GET /api/characters"; then
record_test_result "POST /api/map/acls" "failure" "Failed to get characters"
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -eq 0 ]; then
record_test_result "POST /api/map/acls" "failure" "No characters found"
return 1
fi
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
local acl_name="Auto Test ACL $(date +%s)"
local acl_description="Created by auto_test_api.sh on $(date)"
local data="{\"acl\": {\"name\": \"$acl_name\", \"owner_eve_id\": $CHARACTER_EVE_ID, \"description\": \"$acl_description\"}}"
print_success "Calling API: POST /api/map/acls?slug=$MAP_SLUG"
print_success "Data: $data"
local response
response=$(call_api "POST" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY" "$data")
if ! check_response "$response" "POST /api/map/acls"; then
return 1
fi
local new_acl_id
new_acl_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
local new_api_key
new_api_key=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.api_key // empty')
if [ -n "$new_acl_id" ] && [ -n "$new_api_key" ]; then
record_test_result "POST /api/map/acls" "success" "Created new ACL with ID: $new_acl_id"
SELECTED_ACL_ID=$new_acl_id
ACL_API_KEY=$new_api_key
print_success "Using the new ACL (ID: $SELECTED_ACL_ID) and its API key for further operations"
save_config
return 0
else
record_test_result "POST /api/map/acls" "failure" "Failed to extract ACL ID or API key from response"
return 1
fi
}
test_show_acl() {
print_header "Testing GET /api/acls/:id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "GET /api/acls/:id" "failure" "Missing ACL ID or API key"
return 1
fi
print_success "Calling API: GET /api/acls/$SELECTED_ACL_ID"
local response
response=$(call_api "GET" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY")
if ! check_response "$response" "GET /api/acls/:id"; then
return 1
fi
local acl_name
acl_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
if [ -n "$acl_name" ]; then
record_test_result "GET /api/acls/:id" "success" "Found ACL: $acl_name"
return 0
else
record_test_result "GET /api/acls/:id" "failure" "ACL data not found"
return 1
fi
}
test_update_acl() {
print_header "Testing PUT /api/acls/:id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "PUT /api/acls/:id" "failure" "Missing ACL ID or API key"
return 1
fi
local new_name="Updated Auto Test ACL $(date +%s)"
local new_description="Updated by auto_test_api.sh on $(date)"
local data="{\"acl\": {\"name\": \"$new_name\", \"description\": \"$new_description\"}}"
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID"
print_success "Data: $data"
local response
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY" "$data")
if ! check_response "$response" "PUT /api/acls/:id"; then
return 1
fi
local updated_name
updated_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
if [ "$updated_name" = "$new_name" ]; then
record_test_result "PUT /api/acls/:id" "success" "Updated ACL name to: $updated_name"
return 0
else
record_test_result "PUT /api/acls/:id" "failure" "Failed to update ACL name"
return 1
fi
}
test_create_acl_member() {
print_header "Testing POST /api/acls/:acl_id/members"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "Missing ACL ID or API key"
return 1
fi
if [ -z "$CHARACTER_EVE_ID" ]; then
print_warning "No character EVE ID selected. Fetching characters..."
print_success "Calling API: GET /api/characters"
local characters_response
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$characters_response" "GET /api/characters"; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to get characters"
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -eq 0 ]; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "No characters found"
return 1
fi
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
local data="{\"member\": {\"eve_character_id\": $CHARACTER_EVE_ID, \"role\": \"member\"}}"
print_success "Calling API: POST /api/acls/$SELECTED_ACL_ID/members"
print_success "Data: $data"
local response
response=$(call_api "POST" "/api/acls/$SELECTED_ACL_ID/members" "$ACL_API_KEY" "$data")
if ! check_response "$response" "POST /api/acls/:acl_id/members"; then
return 1
fi
local member_id
member_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
if [ -n "$member_id" ]; then
record_test_result "POST /api/acls/:acl_id/members" "success" "Created new member with ID: $member_id"
MEMBER_ID=$CHARACTER_EVE_ID
return 0
else
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to create member"
return 1
fi
}
test_update_acl_member() {
print_header "Testing PUT /api/acls/:acl_id/members/:member_id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
return 1
fi
local data="{\"member\": {\"role\": \"member\"}}"
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
print_success "Data: $data"
local response
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY" "$data")
if ! check_response "$response" "PUT /api/acls/:acl_id/members/:member_id"; then
return 1
fi
local updated_role
updated_role=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.role // empty')
if [ "$updated_role" = "member" ]; then
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "success" "Updated member role to: $updated_role"
return 0
else
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Failed to update member role"
return 1
fi
}
test_delete_acl_member() {
print_header "Testing DELETE /api/acls/:acl_id/members/:member_id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
return 1
fi
print_success "Calling API: DELETE /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
local response
response=$(call_api "DELETE" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY")
if ! check_response "$response" "DELETE /api/acls/:acl_id/members/:member_id"; then
return 1
fi
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "success" "Deleted member with ID: $MEMBER_ID"
MEMBER_ID=""
return 0
}
test_system_static_info() {
print_header "Testing GET /api/common/system-static-info"
if [ -z "$SELECTED_SYSTEM_ID" ]; then
record_test_result "GET /api/common/system-static-info" "failure" "No system ID selected"
return 1
fi
print_success "Calling API: GET /api/common/system-static-info?id=$SELECTED_SYSTEM_ID"
local response
response=$(call_api "GET" "/api/common/system-static-info?id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/common/system-static-info"; then
return 1
fi
local system_count
system_count=$(echo "$LAST_JSON_RESPONSE" | jq 'length')
record_test_result "GET /api/common/system-static-info" "success" "Found static info for $system_count systems"
return 0
}
#------------------------------------------------------------------------------
# Configuration and Main Menu Functions
#------------------------------------------------------------------------------
set_config() {
print_header "Configuration"
echo -e "Current configuration:"
[ -n "$HOST" ] && echo -e " Host: ${BLUE}$HOST${NC}"
[ -n "$MAP_SLUG" ] && echo -e " Map Slug: ${BLUE}$MAP_SLUG${NC}"
[ -n "$MAP_API_KEY" ] && echo -e " Map API Key: ${BLUE}${MAP_API_KEY:0:8}...${NC}"
read -p "Enter host (default: $HOST): " input_host
[ -n "$input_host" ] && HOST="$input_host"
read -p "Enter map slug: " input_map_slug
[ -n "$input_map_slug" ] && MAP_SLUG="$input_map_slug"
read -p "Enter map API key: " input_map_api_key
[ -n "$input_map_api_key" ] && MAP_API_KEY="$input_map_api_key"
# Reset IDs to force fresh data
SELECTED_SYSTEM_ID=""
SELECTED_ACL_ID=""
ACL_API_KEY=""
CHARACTER_EVE_ID=""
save_config
}
run_all_tests() {
print_header "Running all API tests"
TEST_RESULTS=()
FAILED_TESTS=()
if ! command -v jq &> /dev/null; then
print_error "jq is required for this script to work. Please install it first."
exit 1
fi
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
print_error "Please set MAP_SLUG and MAP_API_KEY before running tests."
exit 1
fi
check_host_reachable
test_list_characters
if test_map_systems; then
test_map_system
else
print_error "Skipping test_map_system because test_map_systems failed"
record_test_result "GET /api/map/system" "failure" "Skipped because test_map_systems failed"
fi
test_map_characters
test_map_structure_timers
test_map_systems_kills
test_map_acls
if test_create_acl; then
test_show_acl
test_update_acl
if test_create_acl_member; then
test_update_acl_member
test_delete_acl_member
else
print_error "Skipping ACL member tests because test_create_acl_member failed"
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
fi
else
print_error "Skipping ACL tests because test_create_acl failed"
record_test_result "GET /api/acls/:id" "failure" "Skipped because test_create_acl failed"
record_test_result "PUT /api/acls/:id" "failure" "Skipped because test_create_acl failed"
record_test_result "POST /api/acls/:acl_id/members" "failure" "Skipped because test_create_acl failed"
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
fi
test_system_static_info
print_header "Test Results"
for result in "${TEST_RESULTS[@]}"; do
echo -e "$result"
done
local total_tests=${#TEST_RESULTS[@]}
local failed_tests=${#FAILED_TESTS[@]}
local passed_tests=$((total_tests - failed_tests))
print_header "Summary"
echo -e "Total tests: $total_tests"
echo -e "Passed: ${GREEN}$passed_tests${NC}"
echo -e "Failed: ${RED}$failed_tests${NC}"
if [ $failed_tests -gt 0 ]; then
print_header "Failed Tests"
for failed in "${FAILED_TESTS[@]}"; do
echo -e "${RED}${NC} $failed"
done
fi
if [ "$VERBOSE_SUMMARY" -eq 1 ]; then
summary_json=$(jq -n --arg total "$total_tests" --arg passed "$passed_tests" --arg failed "$failed_tests" \
'{total_tests: $total_tests|tonumber, passed: $passed|tonumber, failed: $failed|tonumber}')
echo "JSON Summary:"; echo "$summary_json" | jq .
fi
save_config
if [ $failed_tests -gt 0 ]; then
exit 1
else
exit 0
fi
}
#------------------------------------------------------------------------------
# Main Menu and Entry Point
#------------------------------------------------------------------------------
main() {
print_header "Wanderer API Automated Testing Tool"
load_config
if [ -z "$MAP_SLUG" ] || [ -z "$MAP_API_KEY" ]; then
print_warning "MAP_SLUG or MAP_API_KEY not set. Let's configure them now."
set_config
fi
echo -e "What would you like to do?"
echo "1) Run all tests"
echo "2) Set configuration"
echo "3) Exit"
read -p "Enter your choice: " choice
case $choice in
1) run_all_tests ;;
2) set_config ;;
3) exit 0 ;;
*) print_error "Invalid choice"; main ;;
esac
}
# Start the script
main

View File

@@ -1,198 +0,0 @@
#!/usr/bin/env bash
#
# Example script to test your Map & ACL endpoints using curl.
# Requires `jq` to parse JSON responses.
# If any command fails, this script will exit immediately
set -e
#############################################
# Environment Variables (must be set before)
#############################################
: "${BASE_URL:?Need to set BASE_URL, e.g. http://localhost:4444}"
: "${MAP_TOKEN:?Need to set MAP_TOKEN (Bearer token for map requests)}"
: "${MAP_SLUG:?Need to set MAP_SLUG (slug for the map to test)}"
: "${EVE_CHARACTER_ID:?Need to set EVE_CHARACTER_ID (e.g. from /api/characters)}"
echo "Using BASE_URL = $BASE_URL"
echo "Using MAP_TOKEN = $MAP_TOKEN"
echo "Using MAP_SLUG = $MAP_SLUG"
echo "Using EVE_CHARACTER_ID = $EVE_CHARACTER_ID"
echo "-------------------------------------"
#############################################
# 1) Get list of characters (just to confirm they exist)
#############################################
echo
echo "=== 1) Get All Characters (for reference) ==="
curl -s "$BASE_URL/api/characters" | jq
#############################################
# 2) Get ACLs for the given map slug
#############################################
echo
echo "=== 2) List ACLs for Map Slug '$MAP_SLUG' ==="
ACL_LIST_JSON=$(curl -s -H "Authorization: Bearer $MAP_TOKEN" \
"$BASE_URL/api/map/acls?slug=$MAP_SLUG")
echo "$ACL_LIST_JSON" | jq
# Attempt to parse out the first ACL ID and token from the JSON data array:
FIRST_ACL_ID=$(echo "$ACL_LIST_JSON" | jq -r '.data[0].id // empty')
FIRST_ACL_TOKEN=$(echo "$ACL_LIST_JSON" | jq -r '.data[0].api_key // empty')
#############################################
# 3) Decide whether to use an existing ACL or create a new one
#############################################
if [ -z "$FIRST_ACL_ID" ] || [ "$FIRST_ACL_ID" = "null" ]; then
echo "No existing ACL found for map slug: $MAP_SLUG."
USE_EXISTING_ACL=false
else
# We found at least one ACL. But does it have a token?
if [ -z "$FIRST_ACL_TOKEN" ] || [ "$FIRST_ACL_TOKEN" = "null" ]; then
echo "Found an ACL with ID $FIRST_ACL_ID but no api_key in the response."
echo "We cannot do membership actions on it without a token."
USE_EXISTING_ACL=false
else
echo "Parsed ACL_ID from existing ACL: $FIRST_ACL_ID"
echo "Parsed ACL_TOKEN from existing ACL: $FIRST_ACL_TOKEN"
USE_EXISTING_ACL=true
fi
fi
#############################################
# 4) If we cannot use an existing ACL, create a new one
#############################################
if [ "$USE_EXISTING_ACL" = false ]; then
echo
echo "=== Creating a new ACL for membership testing ==="
NEW_ACL_RESPONSE=$(curl -s -X POST \
-H "Authorization: Bearer $MAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"acl": {
"name": "Auto-Created ACL",
"description": "Created because none with a token was found",
"owner_eve_id": "'"$EVE_CHARACTER_ID"'"
}
}' \
"$BASE_URL/api/map/acls?slug=$MAP_SLUG")
echo "New ACL creation response:"
echo "$NEW_ACL_RESPONSE" | jq
ACL_ID=$(echo "$NEW_ACL_RESPONSE" | jq -r '.data.id // empty')
ACL_TOKEN=$(echo "$NEW_ACL_RESPONSE" | jq -r '.data.api_key // empty')
if [ -z "$ACL_ID" ] || [ "$ACL_ID" = "null" ] || \
[ -z "$ACL_TOKEN" ] || [ "$ACL_TOKEN" = "null" ]; then
echo "Failed to create an ACL with a valid token. Exiting..."
exit 1
fi
echo "Newly created ACL_ID: $ACL_ID"
echo "Newly created ACL_TOKEN: $ACL_TOKEN"
else
# Use the existing ACL's details
ACL_ID="$FIRST_ACL_ID"
ACL_TOKEN="$FIRST_ACL_TOKEN"
fi
#############################################
# 5) Show the details of that ACL
#############################################
echo
echo "=== 5) Show ACL Details ==="
ACL_DETAILS=$(curl -s \
-H "Authorization: Bearer $ACL_TOKEN" \
"$BASE_URL/api/acls/$ACL_ID")
echo "$ACL_DETAILS" | jq || {
echo "ACL details response is not valid JSON. Raw response:"
echo "$ACL_DETAILS"
exit 1
}
#############################################
# 6) Create a new ACL member (viewer)
#############################################
echo
echo "=== 6) Create a New ACL Member (viewer) ==="
CREATE_MEMBER_RESP=$(curl -s -X POST \
-H "Authorization: Bearer $ACL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member": {
"eve_character_id": "'"$EVE_CHARACTER_ID"'",
"role": "viewer"
}
}' \
"$BASE_URL/api/acls/$ACL_ID/members")
echo "$CREATE_MEMBER_RESP" | jq || {
echo "Create member response is not valid JSON. Raw response:"
echo "$CREATE_MEMBER_RESP"
exit 1
}
#############################################
# 7) Update the member's role (e.g., admin)
#############################################
echo
echo "=== 7) Update Member Role to 'admin' ==="
UPDATE_MEMBER_RESP=$(curl -s -X PUT \
-H "Authorization: Bearer $ACL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member": {
"role": "admin"
}
}' \
"$BASE_URL/api/acls/$ACL_ID/members/$EVE_CHARACTER_ID")
echo "$UPDATE_MEMBER_RESP" | jq || {
echo "Update member response is not valid JSON. Raw response:"
echo "$UPDATE_MEMBER_RESP"
exit 1
}
#############################################
# 8) Delete the member
#############################################
echo
echo "=== 8) Delete the Member ==="
DELETE_MEMBER_RESP=$(curl -s -X DELETE \
-H "Authorization: Bearer $ACL_TOKEN" \
"$BASE_URL/api/acls/$ACL_ID/members/$EVE_CHARACTER_ID")
echo "$DELETE_MEMBER_RESP" | jq || {
echo "Delete member response is not valid JSON. Raw response:"
echo "$DELETE_MEMBER_RESP"
exit 1
}
#############################################
# 9) (Optional) Update the ACL itself
#############################################
echo
echo "=== 9) Update the ACLs name/description ==="
UPDATED_ACL=$(curl -s -X PUT \
-H "Authorization: Bearer $ACL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"acl": {
"name": "Updated ACL Name (script)",
"description": "An updated description from test script"
}
}' \
"$BASE_URL/api/acls/$ACL_ID")
echo "$UPDATED_ACL" | jq || {
echo "Update ACL response is not valid JSON. Raw response:"
echo "$UPDATED_ACL"
exit 1
}
echo
echo "=== Done! ==="