openapi spec / api updates

This commit is contained in:
Guarzo
2025-11-19 00:10:23 +00:00
parent 5764c41d23
commit 6088afb38c
11 changed files with 185 additions and 42 deletions
+2 -2
View File
@@ -405,7 +405,7 @@ config :wanderer_app, :license_manager,
config :wanderer_app, :sse,
enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true")
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "false")
|> String.to_existing_atom(),
max_connections_total:
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
@@ -420,6 +420,6 @@ config :wanderer_app, :sse,
config :wanderer_app, :external_events,
webhooks_enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true")
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "false")
|> String.to_existing_atom(),
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)
@@ -16,7 +16,8 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
%{id: map_id} ->
Ash.Changeset.force_change_attribute(changeset, :map_id, map_id)
nil ->
_other ->
# nil or unexpected return shape - check for direct map_id
case Ash.Changeset.get_attribute(changeset, :map_id) do
nil ->
Ash.Changeset.add_error(changeset,
-1
View File
@@ -17,7 +17,6 @@ defmodule WandererApp.Env do
def invites(), do: get_key(:invites, false)
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
@decorate cacheable(
+11 -3
View File
@@ -9,7 +9,7 @@ defmodule WandererApp.Map.Operations.Signatures do
alias WandererApp.Map.Server
@spec validate_character_eve_id(map() | nil, String.t()) ::
{:ok, String.t()} | {:error, :invalid_character}
{:ok, String.t()} | {:error, :invalid_character} | {:error, :unexpected_error}
defp validate_character_eve_id(params, fallback_char_id) when is_map(params) do
case Map.get(params, "character_eve_id") do
nil ->
@@ -121,6 +121,10 @@ defmodule WandererApp.Map.Operations.Signatures do
Logger.error("[create_signature] Invalid character_eve_id provided")
{:error, :invalid_character}
{:error, :unexpected_error} ->
Logger.error("[create_signature] Unexpected error during character validation")
{:error, :unexpected_error}
_ ->
Logger.error(
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
@@ -191,9 +195,13 @@ defmodule WandererApp.Map.Operations.Signatures do
Logger.error("[update_signature] Invalid character_eve_id provided")
{:error, :invalid_character}
err ->
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error} ->
Logger.error("[update_signature] Unexpected error during character validation")
{:error, :unexpected_error}
err ->
Logger.error("[update_signature] Signature or system not found: #{inspect(err)}")
{:error, :not_found}
end
end
+55 -8
View File
@@ -12,11 +12,16 @@ defmodule WandererAppWeb.ApiSpecV1 do
# Get the base spec from the original
base_spec = WandererAppWeb.ApiSpec.spec()
# Get v1 spec
# Get v1 spec
v1_spec = WandererAppWeb.OpenApiV1Spec.spec()
# Tag legacy paths and v1 paths appropriately
tagged_legacy_paths = tag_paths(base_spec.paths || %{}, "Legacy API")
# v1 paths already have tags from AshJsonApi, keep them as-is
v1_paths = v1_spec.paths || %{}
# Merge the specs
merged_paths = Map.merge(base_spec.paths || %{}, v1_spec.paths || %{})
merged_paths = Map.merge(tagged_legacy_paths, v1_paths)
# Merge components
merged_components = %Components{
@@ -84,11 +89,53 @@ defmodule WandererAppWeb.ApiSpecV1 do
# Get tags from v1 spec if available
spec_tags = Map.get(v1_spec, :tags, [])
# Add custom v1 tags
v1_label_tags = [
%{name: "v1 JSON:API", description: "JSON:API compliant endpoints with advanced querying"}
]
base_tags ++ v1_label_tags ++ spec_tags
base_tags ++ spec_tags
end
# Tag all operations in paths with the given tag
defp tag_paths(paths, tag) when is_map(paths) do
Map.new(paths, fn {path, path_item} ->
{path, tag_path_item(path_item, tag)}
end)
end
# Handle OpenApiSpex.PathItem structs
defp tag_path_item(%OpenApiSpex.PathItem{} = path_item, tag) do
path_item
|> maybe_tag_operation(:get, tag)
|> maybe_tag_operation(:put, tag)
|> maybe_tag_operation(:post, tag)
|> maybe_tag_operation(:delete, tag)
|> maybe_tag_operation(:patch, tag)
|> maybe_tag_operation(:options, tag)
|> maybe_tag_operation(:head, tag)
end
# Handle plain maps (from AshJsonApi)
defp tag_path_item(path_item, tag) when is_map(path_item) do
Map.new(path_item, fn {method, operation} ->
{method, add_tag_to_operation(operation, tag)}
end)
end
defp tag_path_item(path_item, _tag), do: path_item
defp maybe_tag_operation(path_item, method, tag) do
case Map.get(path_item, method) do
nil -> path_item
operation -> Map.put(path_item, method, add_tag_to_operation(operation, tag))
end
end
defp add_tag_to_operation(%OpenApiSpex.Operation{} = operation, tag) do
%{operation | tags: [tag | List.wrap(operation.tags)]}
end
defp add_tag_to_operation(%{} = operation, tag) do
Map.update(operation, :tags, [tag], fn existing_tags ->
[tag | List.wrap(existing_tags)]
end)
end
defp add_tag_to_operation(operation, _tag), do: operation
end
@@ -0,0 +1,21 @@
defmodule WandererAppWeb.Plugs.CheckWebhooksDisabled do
@moduledoc """
Plug to check if webhooks are enabled.
This plug blocks access to webhook management endpoints when webhooks are disabled.
Enable webhooks by setting WANDERER_WEBHOOKS_ENABLED=true in your environment.
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if not WandererApp.Env.webhooks_enabled?() do
conn
|> send_resp(403, "Webhooks are disabled. Set WANDERER_WEBHOOKS_ENABLED=true to enable.")
|> halt()
else
conn
end
end
end
@@ -1,15 +0,0 @@
defmodule WandererAppWeb.Plugs.CheckWebsocketDisabled do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if not WandererApp.Env.websocket_events_enabled?() do
conn
|> send_resp(403, "WebSocket events are disabled")
|> halt()
else
conn
end
end
end
+4 -4
View File
@@ -201,8 +201,8 @@ defmodule WandererAppWeb.Router do
plug WandererAppWeb.Plugs.CheckCharacterApiDisabled
end
pipeline :api_websocket_events do
plug WandererAppWeb.Plugs.CheckWebsocketDisabled
pipeline :api_webhooks do
plug WandererAppWeb.Plugs.CheckWebhooksDisabled
end
pipeline :api_acl do
@@ -302,9 +302,9 @@ defmodule WandererAppWeb.Router do
get "/tracked-characters", MapAPIController, :show_tracked_characters
end
# WebSocket events and webhook management endpoints (disabled by default)
# Webhook management endpoints (requires WANDERER_WEBHOOKS_ENABLED=true)
scope "/api/maps/:map_identifier", WandererAppWeb do
pipe_through [:api, :api_map, :api_websocket_events]
pipe_through [:api, :api_map, :api_webhooks]
get "/events", MapEventsAPIController, :list_events
+18
View File
@@ -42,6 +42,24 @@ In the dynamic world of EVE Online wormhole mapping, every second counts. When a
- Your map API token (found in map settings)
- Basic programming knowledge for integration
### Server Configuration (Community Edition)
If you're running Wanderer Community Edition (CE), you need to enable the required features via environment variables:
**For SSE (Server-Sent Events):**
```bash
WANDERER_SSE_ENABLED=true
```
**For Webhooks:**
```bash
WANDERER_WEBHOOKS_ENABLED=true
```
Add these to your `.env` file or Docker environment configuration and restart Wanderer. Without these settings, you'll receive a 403 error when trying to access SSE streams or webhook management endpoints.
*Note: The public Wanderer instance at wanderer.ltd has these features enabled by default.*
### Authentication
Both SSE and webhook APIs use your existing map API token for authentication. This token should be kept secure and never exposed in client-side code.
+19 -1
View File
@@ -81,6 +81,24 @@ You can find or generate your map's API key in the map settings within the Wande
**Session Authentication:**
Web clients can also use session-based authentication for interactive use, maintaining compatibility with existing browser-based integrations.
### Server Configuration (Community Edition)
If you're running Wanderer Community Edition (CE), ensure the following environment variables are configured:
**Required for API access:**
```bash
WANDERER_PUBLIC_API_DISABLED=false # Enable public API (default: false)
```
**Optional features:**
```bash
WANDERER_SSE_ENABLED=true # Enable Server-Sent Events (default: false)
WANDERER_WEBHOOKS_ENABLED=true # Enable webhook management (default: false)
WANDERER_CHARACTER_API_DISABLED=false # Enable character API (default: true)
```
Add these to your `.env` file or Docker environment configuration and restart Wanderer.
## JSON:API Features
### Resource Relationships
@@ -143,7 +161,7 @@ GET /api/v1/user_activities?include=character&sort=-inserted_at&page[limit]=15&p
The API v1 provides access to over 25 resources through the Ash Framework. Here are the primary resources:
### Core Resources
- **Maps** (`/api/v1/maps`) - Map management with full CRUD operations
- **Maps** (`/api/v1/maps/:slug`) - Map management with create, read, update, and delete operations (accessed by slug with map-specific API key; no listing endpoint)
- **Access Lists** (`/api/v1/access_lists`) - ACL management and permissions with full CRUD operations
- **Access List Members** (`/api/v1/access_list_members`) - ACL member management with full CRUD operations
- **Map Access Lists** (`/api/v1/map_access_lists`) - Map-ACL associations with full CRUD operations
@@ -3,20 +3,40 @@ defmodule WandererApp.Repo.Migrations.AddPublicApiKeyUniqueIndex do
Adds a unique index on the public_api_key column of maps_v1.
This migration:
1. Creates a unique index on public_api_key where the value is not null
2. Allows multiple NULL values (maps without API keys)
3. Ensures all non-NULL API keys are unique
1. Creates a backup table (maps_v1_api_key_backup) for data safety
2. Backs up and clears duplicate API keys (keeping the oldest by inserted_at)
3. Creates a unique index on public_api_key where the value is not null
4. Allows multiple NULL values (maps without API keys)
5. Ensures all non-NULL API keys are unique
The partial index (WHERE public_api_key IS NOT NULL) is used because:
- Most maps won't have an API key set
- We only care about uniqueness for maps that do have one
- PostgreSQL's unique constraints on nullable columns already allow multiple NULLs,
but a partial index is more explicit and efficient
## Data Recovery
If you need to restore cleared API keys, query the backup table:
SELECT map_id, old_public_api_key, backed_up_at
FROM maps_v1_api_key_backup
WHERE reason = 'duplicate_api_key_cleared_for_unique_index';
To restore a specific map's API key:
UPDATE maps_v1 SET public_api_key = '<old_key>'
WHERE id = '<map_id>';
Note: Restoring will cause uniqueness conflicts if duplicates still exist.
"""
use Ecto.Migration
def up do
# First, check for any duplicate non-null API keys and handle them
# Create backup table before any destructive changes
create_backup_table()
# Check for any duplicate non-null API keys and handle them (with backup)
check_and_fix_duplicates()
# Create the unique index
@@ -31,6 +51,20 @@ defmodule WandererApp.Repo.Migrations.AddPublicApiKeyUniqueIndex do
IO.puts("Created unique index on maps_v1.public_api_key")
end
defp create_backup_table do
repo().query!("""
CREATE TABLE IF NOT EXISTS maps_v1_api_key_backup (
id UUID PRIMARY KEY,
map_id UUID NOT NULL,
old_public_api_key TEXT NOT NULL,
reason TEXT NOT NULL,
backed_up_at TIMESTAMP NOT NULL DEFAULT NOW()
)
""", [])
IO.puts("Created backup table maps_v1_api_key_backup")
end
def down do
drop_if_exists(
index(:maps_v1, [:public_api_key],
@@ -39,6 +73,10 @@ defmodule WandererApp.Repo.Migrations.AddPublicApiKeyUniqueIndex do
)
IO.puts("Dropped unique index on maps_v1.public_api_key")
# Drop backup table
repo().query!("DROP TABLE IF EXISTS maps_v1_api_key_backup", [])
IO.puts("Dropped backup table maps_v1_api_key_backup")
end
defp check_and_fix_duplicates do
@@ -76,20 +114,28 @@ defmodule WandererApp.Repo.Migrations.AddPublicApiKeyUniqueIndex do
[_keep | clear_ids] = Enum.map(id_rows, fn [id] -> id end)
Enum.each(clear_ids, fn id ->
# Backup the API key before clearing
backup_query = """
INSERT INTO maps_v1_api_key_backup (id, map_id, old_public_api_key, reason)
VALUES (gen_random_uuid(), $1::uuid, $2, 'duplicate_api_key_cleared_for_unique_index')
"""
repo().query!(backup_query, [id, api_key])
# Clear the duplicate
clear_query = "UPDATE maps_v1 SET public_api_key = NULL WHERE id::text = $1"
repo().query!(clear_query, [id])
IO.puts(" Cleared API key for map #{id}")
IO.puts(" Backed up and cleared API key for map #{id}")
end)
{:error, error} ->
IO.puts("Error getting IDs: #{inspect(error)}")
raise "Failed to get duplicate IDs for key: #{inspect(error)}"
end
end)
IO.puts("Duplicate API keys cleared")
{:error, error} ->
IO.puts("Error checking for duplicates: #{inspect(error)}")
raise "Failed to check for duplicate keys: #{inspect(error)}"
end
end
end