mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-09-26 23:16:10 +00:00
openapi spec / api updates
This commit is contained in:
+2
-2
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user