diff --git a/.gitignore b/.gitignore index 0c7beae4..d5b16e17 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ .env *.local.env +test/manual/.auto* .direnv/ .cache/ diff --git a/assets/static/images/news/03-05-api/swagger-ui.png b/assets/static/images/news/03-05-api/swagger-ui.png new file mode 100755 index 00000000..c034b734 Binary files /dev/null and b/assets/static/images/news/03-05-api/swagger-ui.png differ diff --git a/lib/wanderer_app_web/api_spec.ex b/lib/wanderer_app_web/api_spec.ex new file mode 100644 index 00000000..2d0a2fc6 --- /dev/null +++ b/lib/wanderer_app_web/api_spec.ex @@ -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 diff --git a/lib/wanderer_app_web/controllers/access_list_api_controller.ex b/lib/wanderer_app_web/controllers/access_list_api_controller.ex index 1f85562a..b9229e6c 100644 --- a/lib/wanderer_app_web/controllers/access_list_api_controller.ex +++ b/lib/wanderer_app_web/controllers/access_list_api_controller.ex @@ -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} -> diff --git a/lib/wanderer_app_web/controllers/access_list_member_api_controller.ex b/lib/wanderer_app_web/controllers/access_list_member_api_controller.ex index 43949b7a..9d1a6cd5 100644 --- a/lib/wanderer_app_web/controllers/access_list_member_api_controller.ex +++ b/lib/wanderer_app_web/controllers/access_list_member_api_controller.ex @@ -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 } diff --git a/lib/wanderer_app_web/controllers/character_api_controller.ex b/lib/wanderer_app_web/controllers/character_api_controller.ex index 60e97ccc..f741738a 100644 --- a/lib/wanderer_app_web/controllers/character_api_controller.ex +++ b/lib/wanderer_app_web/controllers/character_api_controller.ex @@ -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} -> diff --git a/lib/wanderer_app_web/controllers/common_api_controller.ex b/lib/wanderer_app_web/controllers/common_api_controller.ex index b735fa88..9405f317 100644 --- a/lib/wanderer_app_web/controllers/common_api_controller.ex +++ b/lib/wanderer_app_web/controllers/common_api_controller.ex @@ -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= - - Requires 'id' (the solar_system_id). - - Example: - GET /api/common/system_static?id=31002229 + GET /api/common/system-static-info?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([ diff --git a/lib/wanderer_app_web/controllers/map_api_controller.ex b/lib/wanderer_app_web/controllers/map_api_controller.ex index 423fc4fb..a8893b15 100644 --- a/lib/wanderer_app_web/controllers/map_api_controller.ex +++ b/lib/wanderer_app_web/controllers/map_api_controller.ex @@ -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=&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= GET /api/map/systems_kills?map_id=&hours_ago= """ + @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 diff --git a/lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex b/lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex index e1cc14eb..711a7896 100644 --- a/lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex +++ b/lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex @@ -1,7 +1,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do @moduledoc """ A plug that checks the "Authorization: Bearer " header - against the map’s 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 diff --git a/lib/wanderer_app_web/router.ex b/lib/wanderer_app_web/router.ex index f4a3856b..7c021f31 100644 --- a/lib/wanderer_app_web/router.ex +++ b/lib/wanderer_app_web/router.ex @@ -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 # diff --git a/mix.exs b/mix.exs index fdc9f3ad..1e1b1abb 100644 --- a/mix.exs +++ b/mix.exs @@ -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"}, diff --git a/mix.lock b/mix.lock index eded1995..9472b5f5 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/priv/posts/2025/02-20-acl-api.md b/priv/posts/2025/02-20-acl-api.md index bc98c72a..cdb3e576 100644 --- a/priv/posts/2025/02-20-acl-api.md +++ b/priv/posts/2025/02-20-acl-api.md @@ -10,7 +10,7 @@ ## Introduction -Wanderer’s expanded public API now lets you retrieve **all characters** in the system and manage “Access Lists” (ACLs) for controlling visibility or permissions. These endpoints allow you to: +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 ``` -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.)_ +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 " \ "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 " \ + -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 Member’s Role +### 7. Change a Member's Role ```bash PUT /api/acls/:acl_id/members/:member_id ``` -- **Description:** Updates an ACL member’s role (e.g. from `viewer` to `admin`). +- **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 member’s 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. diff --git a/priv/posts/2025/03-05-api.md b/priv/posts/2025/03-05-api.md new file mode 100644 index 00000000..2464bcdd --- /dev/null +++ b/priv/posts/2025/03-05-api.md @@ -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 +``` + +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= +GET /api/map/systems?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 " \ + "https://wanderer.example.com/api/map/systems?slug=some-slug" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": "", + "name": "", + "status": 0, + "tag": null, + "visible": false, + "description": null, + "labels": "", + "inserted_at": "2025-01-01T13:38:42.875843Z", + "updated_at": "2025-01-01T13:40:16.750234Z", + "locked": false, + "solar_system_id": "", + "map_id": "", + "custom_name": null, + "position_x": 1125, + "position_y": -285 + }, + ... + ] +} +``` + +### 2. Show Single System + +```bash +GET /api/map/system?id=&map_id= +GET /api/map/system?id=&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 " \ + "https://wanderer.example.com/api/map/system?id=&slug=" +``` + +#### Example Response + +```json +{ + "data": { + "id": "", + "name": "", + "status": 0, + "tag": null, + "visible": false, + "description": null, + "labels": "", + "inserted_at": "2025-01-03T06:30:02.069090Z", + "updated_at": "2025-01-03T07:47:07.471051Z", + "locked": false, + "solar_system_id": "", + "map_id": "", + "custom_name": null, + "position_x": 1005, + "position_y": 765 + } +} +``` + +### 3. System Static Information + +```bash +GET /api/common/system-static-info?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= +GET /api/map/characters?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 " \ + "https://wanderer.example.com/api/map/characters?slug=some-slug" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": "", + "character": { + "id": "", + "name": "", + "inserted_at": "2025-01-01T05:24:18.461721Z", + "updated_at": "2025-01-03T07:45:52.294052Z", + "alliance_id": "", + "alliance_name": "", + "alliance_ticker": "", + "corporation_id": "", + "corporation_name": "", + "corporation_ticker": "", + "eve_id": "" + }, + "tracked": true, + "map_id": "" + }, + ... + ] +} +``` + +### 5. Kills Activity + +```bash +GET /api/map/systems-kills?map_id= +GET /api/map/systems-kills?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 " \ + "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= +GET /api/map/structure-timers?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 " \ + "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= +GET /api/map/acls?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 " \ + "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 " \ + "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 " \ + -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 " \ + -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 " \ + -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 " \ + -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 " \ + -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 " \ + -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 " \ + "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** \ No newline at end of file diff --git a/test/manual/.api_test_config b/test/manual/.api_test_config new file mode 100644 index 00000000..fbb0becc --- /dev/null +++ b/test/manual/.api_test_config @@ -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="" diff --git a/test/manual/.api_test_config.example b/test/manual/.api_test_config.example new file mode 100644 index 00000000..5540985d --- /dev/null +++ b/test/manual/.api_test_config.example @@ -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" \ No newline at end of file diff --git a/test/manual/.auto_api_test_config b/test/manual/.auto_api_test_config new file mode 100644 index 00000000..90eaaeb7 --- /dev/null +++ b/test/manual/.auto_api_test_config @@ -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" diff --git a/test/manual/auto_test_api.sh b/test/manual/auto_test_api.sh new file mode 100755 index 00000000..49181535 --- /dev/null +++ b/test/manual/auto_test_api.sh @@ -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 diff --git a/test/manual/test_api_calls.sh b/test/manual/test_api_calls.sh deleted file mode 100755 index 203040c8..00000000 --- a/test/manual/test_api_calls.sh +++ /dev/null @@ -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 ACL’s 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! ==="