Compare commits

..

17 Commits

Author SHA1 Message Date
DanSylvest
98c54a3413 fix(Map): Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset. 2025-11-13 12:53:40 +03:00
CI
0439110938 chore: [skip ci] 2025-11-13 07:52:33 +00:00
CI
8ce1e5fa3e chore: release version v1.84.13 2025-11-13 07:52:33 +00:00
Dmitry Popov
ebaf6bcdc6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-13 08:52:00 +01:00
Dmitry Popov
40d947bebc chore: updated RELEASE_NODE for server defaults 2025-11-13 08:51:56 +01:00
CI
61d1c3848f chore: [skip ci] 2025-11-13 07:39:29 +00:00
CI
e152ce179f chore: release version v1.84.12 2025-11-13 07:39:29 +00:00
Dmitry Popov
7bbe387183 chore: reduce garbage collection interval 2025-11-13 08:38:52 +01:00
CI
b1555ff03c chore: [skip ci] 2025-11-12 18:53:48 +00:00
CI
e624499244 chore: release version v1.84.11 2025-11-12 18:53:48 +00:00
Dmitry Popov
6a1976dec6 Merge pull request #541 from guarzo/guarzo/apifun2
fix: api and doc updates
2025-11-12 22:53:17 +04:00
Guarzo
3db24c4344 fix: api and doc updates 2025-11-12 18:39:21 +00:00
CI
883c09f255 chore: [skip ci] 2025-11-12 17:28:54 +00:00
CI
ff24d80038 chore: release version v1.84.10 2025-11-12 17:28:54 +00:00
Dmitry Popov
63cbc9c0b9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-12 18:28:20 +01:00
Dmitry Popov
8056972a27 fix(core): Fixed adding system on character dock 2025-11-12 18:28:16 +01:00
CI
1759d46740 chore: [skip ci] 2025-11-12 13:28:14 +00:00
22 changed files with 785 additions and 697 deletions

View File

@@ -1,5 +1,7 @@
export WEB_APP_URL="http://localhost:8000"
export RELEASE_COOKIE="PDpbnyo6mEI_0T4ZsHH_ESmi1vT1toQ8PTc0vbfg5FIT4Ih-Lh98mw=="
# Erlang node name for distributed Erlang (optional - defaults to wanderer@hostname)
# export RELEASE_NODE="wanderer@localhost"
export EVE_CLIENT_ID="<EVE_CLIENT_ID>"
export EVE_CLIENT_SECRET="<EVE_CLIENT_SECRET>"
export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"

View File

@@ -2,6 +2,34 @@
<!-- changelog -->
## [v1.84.13](https://github.com/wanderer-industries/wanderer/compare/v1.84.12...v1.84.13) (2025-11-13)
## [v1.84.12](https://github.com/wanderer-industries/wanderer/compare/v1.84.11...v1.84.12) (2025-11-13)
## [v1.84.11](https://github.com/wanderer-industries/wanderer/compare/v1.84.10...v1.84.11) (2025-11-12)
### Bug Fixes:
* api and doc updates
## [v1.84.10](https://github.com/wanderer-industries/wanderer/compare/v1.84.9...v1.84.10) (2025-11-12)
### Bug Fixes:
* core: Fixed adding system on character dock
## [v1.84.9](https://github.com/wanderer-industries/wanderer/compare/v1.84.8...v1.84.9) (2025-11-12)

View File

@@ -4,10 +4,13 @@ import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constant
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const MapInterface = () => {
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsSettings, updateWidgetSettings } = useMapRootState();
const items = useMemo(() => {
if (Object.keys(windowsSettings).length === 0) {
return [];
}
return windowsSettings.windows
.map(x => {
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;

View File

@@ -10,9 +10,14 @@ import { useCallback } from 'react';
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const {
storedSettings: { resetSettings },
} = useMapRootState();
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const renderSettingsList = useCallback(
@@ -22,7 +27,7 @@ export const CommonSettings = () => {
[renderSettingItem],
);
const handleResetSettings = () => {};
const handleResetSettings = useCallback(() => resetSettings(), [resetSettings]);
return (
<div className="flex flex-col h-full gap-1">

View File

@@ -6,9 +6,11 @@ import {
MapUnionTypes,
OutCommandHandler,
SolarSystemConnection,
StringBoolean,
TrackingCharacter,
UseCharactersCacheData,
UseCommentsData,
UserPermission,
} from '@/hooks/Mapper/types';
import { useCharactersCache, useComments, useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
@@ -80,7 +82,16 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
options: {},
options: {
allowed_copy_for: UserPermission.VIEW_SYSTEM,
allowed_paste_for: UserPermission.VIEW_SYSTEM,
layout: '',
restrict_offline_showing: 'false',
show_linked_signature_id: 'false',
show_linked_signature_id_temp_name: 'false',
show_temp_system_name: 'false',
store_custom_labels: 'false',
},
isSubscriptionActive: false,
linkSignatureToSystem: null,
mainCharacterEveId: null,
@@ -135,7 +146,7 @@ export interface MapRootContextProps {
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
resetSettings(settings: MapUserSettings): void;
resetSettings(): void;
checkOldSettings(): void;
};
}

View File

@@ -148,10 +148,6 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
}, []);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
const getSettingsForExport = useCallback(() => {
const { map_slug } = ref.current;
@@ -166,6 +162,24 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
applySettings(createDefaultStoredSettings());
}, [applySettings]);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
// IN Case if in runtime someone clear settings
useEffect(() => {
if (Object.keys(windowsSettings).length !== 0) {
return;
}
if (!isReady) {
return;
}
resetSettings();
location.reload();
}, [isReady, resetSettings, windowsSettings]);
return {
isReady,
hasOldSettings,

View File

@@ -16,7 +16,7 @@ defmodule WandererApp.Map.MapPool do
@registry :map_pool_registry
@unique_registry :unique_map_pool_registry
@garbage_collection_interval :timer.hours(12)
@garbage_collection_interval :timer.hours(4)
@systems_cleanup_timeout :timer.minutes(30)
@characters_cleanup_timeout :timer.minutes(5)
@connections_cleanup_timeout :timer.minutes(5)

View File

@@ -8,22 +8,23 @@ defmodule WandererApp.Map.Operations.Signatures do
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
alias WandererApp.Map.Server
# Private helper to validate character_eve_id from params
# If character_eve_id is provided in params, validates it exists in the system
# If not provided, falls back to the owner's character ID
# Private helper to validate character_eve_id from params and return internal character ID
# If character_eve_id is provided in params, validates it exists and returns the internal UUID
# If not provided, falls back to the owner's character ID (which is already the internal UUID)
@spec validate_character_eve_id(map() | nil, String.t()) ::
{:ok, String.t()} | {:error, :invalid_character}
defp validate_character_eve_id(params, fallback_char_id) when is_map(params) do
case Map.get(params, "character_eve_id") do
nil ->
# No character_eve_id provided, use fallback (owner's character)
# No character_eve_id provided, use fallback (owner's internal character UUID)
{:ok, fallback_char_id}
provided_char_id when is_binary(provided_char_id) ->
# Validate the provided character_eve_id exists
case Character.by_eve_id(provided_char_id) do
{:ok, _character} ->
{:ok, provided_char_id}
provided_char_eve_id when is_binary(provided_char_eve_id) ->
# Validate the provided character_eve_id exists and get internal UUID
case Character.by_eve_id(provided_char_eve_id) do
{:ok, character} ->
# Return the internal character UUID, not the eve_id
{:ok, character.id}
_ ->
{:error, :invalid_character}
@@ -74,11 +75,13 @@ defmodule WandererApp.Map.Operations.Signatures do
)
when is_integer(solar_system_id) do
# Validate character first, then convert solar_system_id to system_id
with {:ok, validated_char_id} <- validate_character_eve_id(params, char_id),
# validated_char_uuid is the internal character UUID for Server.update_signatures
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
{:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
# Keep character_eve_id in attrs if provided by user (parse_signatures will use it)
# If not provided, parse_signatures will use the character_eve_id from validated_char_uuid lookup
attrs =
params
|> Map.put("character_eve_id", validated_char_id)
|> Map.put("system_id", system.id)
|> Map.delete("solar_system_id")
@@ -87,7 +90,7 @@ defmodule WandererApp.Map.Operations.Signatures do
updated_signatures: [],
removed_signatures: [],
solar_system_id: solar_system_id,
character_id: validated_char_id,
character_id: validated_char_uuid, # Pass internal UUID here
user_id: user_id,
delete_connection_with_sigs: false
}) do
@@ -149,7 +152,8 @@ defmodule WandererApp.Map.Operations.Signatures do
params
) do
# Validate character first, then look up signature and system
with {:ok, validated_char_id} <- validate_character_eve_id(params, char_id),
# validated_char_uuid is the internal character UUID
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
{:ok, sig} <- MapSystemSignature.by_id(sig_id),
{:ok, system} <- MapSystem.by_id(sig.system_id) do
base = %{
@@ -159,11 +163,11 @@ defmodule WandererApp.Map.Operations.Signatures do
"group" => sig.group,
"type" => sig.type,
"custom_info" => sig.custom_info,
"character_eve_id" => validated_char_id,
"description" => sig.description,
"linked_system_id" => sig.linked_system_id
}
# Merge user params (which may include character_eve_id) with base
attrs = Map.merge(base, params)
:ok =
@@ -172,7 +176,7 @@ defmodule WandererApp.Map.Operations.Signatures do
updated_signatures: [attrs],
removed_signatures: [],
solar_system_id: system.solar_system_id,
character_id: validated_char_id,
character_id: validated_char_uuid, # Pass internal UUID here
user_id: user_id,
delete_connection_with_sigs: false
})

View File

@@ -310,8 +310,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
start_solar_system_id =
WandererApp.Cache.take("map:#{map_id}:character:#{character_id}:start_solar_system_id")
case is_nil(old_location.solar_system_id) and
is_nil(start_solar_system_id) and
case is_nil(old_location.solar_system_id) &&
is_nil(start_solar_system_id) &&
ConnectionsImpl.can_add_location(scope, location.solar_system_id) do
true ->
:ok = SystemsImpl.maybe_add_system(map_id, location, nil, map_opts)

View File

@@ -657,12 +657,14 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
)
)
def is_connection_valid(:all, _from_solar_system_id, _to_solar_system_id), do: true
def is_connection_valid(:all, from_solar_system_id, to_solar_system_id),
do: from_solar_system_id != to_solar_system_id
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) and
from_solar_system_id != to_solar_system_id do
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do

View File

@@ -279,7 +279,8 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
group: sig["group"],
type: Map.get(sig, "type"),
custom_info: Map.get(sig, "custom_info"),
character_eve_id: character_eve_id,
# Use character_eve_id from sig if provided, otherwise use the default
character_eve_id: Map.get(sig, "character_eve_id", character_eve_id),
deleted: false
}
end)

View File

@@ -12,28 +12,32 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
# Inlined OpenAPI schema for a map system signature
@signature_schema %OpenApiSpex.Schema{
title: "MapSystemSignature",
description: "A cosmic signature scanned in an EVE Online solar system",
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
solar_system_id: %OpenApiSpex.Schema{type: :integer},
eve_id: %OpenApiSpex.Schema{type: :string},
character_eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string, nullable: true},
description: %OpenApiSpex.Schema{type: :string, nullable: true},
type: %OpenApiSpex.Schema{type: :string, nullable: true},
linked_system_id: %OpenApiSpex.Schema{type: :integer, nullable: true},
kind: %OpenApiSpex.Schema{type: :string, nullable: true},
group: %OpenApiSpex.Schema{type: :string, nullable: true},
custom_info: %OpenApiSpex.Schema{type: :string, nullable: true},
updated: %OpenApiSpex.Schema{type: :integer, nullable: true},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
id: %OpenApiSpex.Schema{type: :string, format: :uuid, description: "Unique signature identifier"},
solar_system_id: %OpenApiSpex.Schema{type: :integer, description: "EVE Online solar system ID"},
eve_id: %OpenApiSpex.Schema{type: :string, description: "In-game signature ID (e.g., ABC-123)"},
character_eve_id: %OpenApiSpex.Schema{
type: :string,
description: "EVE character ID who scanned/updated this signature. Must be a valid character in the database. If not provided, defaults to the map owner's character.",
nullable: true
},
name: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Signature name"},
description: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Additional notes"},
type: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Signature type"},
linked_system_id: %OpenApiSpex.Schema{type: :integer, nullable: true, description: "Connected solar system ID for wormholes"},
kind: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Signature kind (e.g., cosmic_signature)"},
group: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Signature group (e.g., wormhole, data, relic)"},
custom_info: %OpenApiSpex.Schema{type: :string, nullable: true, description: "Custom metadata"},
updated: %OpenApiSpex.Schema{type: :integer, nullable: true, description: "Update counter"},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time, description: "Creation timestamp"},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time, description: "Last update timestamp"}
},
required: [
:id,
:solar_system_id,
:eve_id,
:character_eve_id
:eve_id
],
example: %{
id: "sig-uuid-1",
@@ -143,6 +147,10 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
@doc """
Create a new signature.
The `character_eve_id` field is optional. If provided, it must be a valid character
that exists in the database, otherwise a 422 error will be returned. If not provided,
the signature will be associated with the map owner's character.
"""
operation(:create,
summary: "Create a new signature",
@@ -162,6 +170,18 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
type: :object,
properties: %{data: @signature_schema},
example: %{data: @signature_schema.example}
}},
unprocessable_entity:
{"Validation error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{
type: :string,
description: "Error type (e.g., 'invalid_character', 'system_not_found', 'missing_params')"
}
},
example: %{error: "invalid_character"}
}}
]
)
@@ -175,6 +195,9 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
@doc """
Update a signature by ID.
The `character_eve_id` field is optional. If provided, it must be a valid character
that exists in the database, otherwise a 422 error will be returned.
"""
operation(:update,
summary: "Update a signature by ID",
@@ -195,6 +218,18 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
type: :object,
properties: %{data: @signature_schema},
example: %{data: @signature_schema.example}
}},
unprocessable_entity:
{"Validation error", "application/json",
%OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{
type: :string,
description: "Error type (e.g., 'invalid_character', 'unexpected_error')"
}
},
example: %{error: "invalid_character"}
}}
]
)

View File

@@ -149,12 +149,12 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
end
defp validate_api_token(conn, token) do
# Check for map identifier in path params
# According to PR feedback, routes supply params["map_identifier"]
case conn.params["map_identifier"] do
# Try to get map identifier from multiple sources
map_identifier = get_map_identifier(conn)
case map_identifier do
nil ->
# No map identifier in path - this might be a general API endpoint
# For now, we'll return an error since we need to validate against a specific map
# No map identifier found - this might be a general API endpoint
{:error, "Authentication failed", :no_map_context}
identifier ->
@@ -182,6 +182,37 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
end
end
# Extract map identifier from multiple sources
defp get_map_identifier(conn) do
# 1. Check path params (e.g., /api/v1/maps/:map_identifier/systems)
case conn.params["map_identifier"] do
id when is_binary(id) and id != "" -> id
_ ->
# 2. Check request body for map_id (JSON:API format)
case conn.body_params do
%{"data" => %{"attributes" => %{"map_id" => map_id}}} when is_binary(map_id) and map_id != "" ->
map_id
%{"data" => %{"relationships" => %{"map" => %{"data" => %{"id" => map_id}}}}} when is_binary(map_id) and map_id != "" ->
map_id
# 3. Check flat body params (non-JSON:API format)
%{"map_id" => map_id} when is_binary(map_id) and map_id != "" ->
map_id
_ ->
# 4. Check query params (e.g., ?filter[map_id]=...)
case conn.params do
%{"filter" => %{"map_id" => map_id}} when is_binary(map_id) and map_id != "" ->
map_id
_ ->
nil
end
end
end
end
# Helper to resolve map by ID or slug
defp resolve_map_identifier(identifier) do
# Try as UUID first

View File

@@ -1,100 +0,0 @@
defmodule WandererAppWeb.Plugs.ConditionalAssignMapOwner do
@moduledoc """
Conditionally assigns map owner information to conn.assigns for V1 API routes.
This plug enables PubSub broadcasting for map operations by ensuring owner_character_id
and owner_user_id are available when map context exists.
Unlike the standard :api_map pipeline plugs (CheckMapApiKey, CheckMapSubscription),
this plug does NOT halt the request if map context is missing, making it safe to use
for both map-specific and user-level resources.
Map context detection (in order of priority):
1. conn.assigns[:map_id] - Set by CheckJsonApiAuth for Bearer token requests with map_identifier
2. filter[map_id] - JSON:API filter parameter for map-specific queries
3. Request body map_id - For create/update operations on map resources
If no map context is found, the plug simply continues without setting owner fields.
This allows user-level resources (AccessList, UserActivity, etc.) to work normally.
"""
import Plug.Conn
alias WandererApp.Map.Operations
def init(opts), do: opts
def call(conn, _opts) do
case get_map_id(conn) do
{:ok, map_id} ->
# Map context found - fetch and assign owner information
case Operations.get_owner_character_id(map_id) do
{:ok, %{id: char_id, user_id: user_id}} ->
conn
|> assign(:map_id, map_id)
|> assign(:owner_character_id, char_id)
|> assign(:owner_user_id, user_id)
_ ->
# Map exists but owner not found - set nil values
conn
|> assign(:map_id, map_id)
|> assign(:owner_character_id, nil)
|> assign(:owner_user_id, nil)
end
:no_map_context ->
# No map context - this is okay for user-level resources
# Don't halt, just continue without setting map fields
conn
end
end
# Try to extract map_id from various sources
defp get_map_id(conn) do
# 1. Check if already set by CheckJsonApiAuth (Bearer token with map_identifier)
case conn.assigns[:map_id] do
map_id when is_binary(map_id) and map_id != "" ->
{:ok, map_id}
_ ->
# 2. Check JSON:API filter parameters (e.g., filter[map_id]=uuid)
case get_filter_map_id(conn) do
{:ok, map_id} -> {:ok, map_id}
:not_found -> check_body_map_id(conn)
end
end
end
# Extract map_id from JSON:API filter parameters
defp get_filter_map_id(conn) do
# JSON:API filters come as filter[map_id]=value
case conn.params do
%{"filter" => %{"map_id" => map_id}} when is_binary(map_id) and map_id != "" ->
{:ok, map_id}
_ ->
:not_found
end
end
# Extract map_id from request body (for create/update operations)
defp check_body_map_id(conn) do
case conn.body_params do
%{"data" => %{"attributes" => %{"map_id" => map_id}}}
when is_binary(map_id) and map_id != "" ->
{:ok, map_id}
%{"data" => %{"relationships" => %{"map" => %{"data" => %{"id" => map_id}}}}}
when is_binary(map_id) and map_id != "" ->
{:ok, map_id}
# Also check flat params for non-JSON:API formatted requests
%{"map_id" => map_id} when is_binary(map_id) and map_id != "" ->
{:ok, map_id}
_ ->
:no_map_context
end
end
end

View File

@@ -10,529 +10,8 @@ defmodule WandererAppWeb.OpenApiV1Spec do
@impl OpenApiSpex.OpenApi
def spec do
# This is called by the modify_open_api option in the router
# We should return the spec from WandererAppWeb.OpenApi module
# We delegate to WandererAppWeb.OpenApi module which generates
# the spec from AshJsonApi with custom endpoints merged in
WandererAppWeb.OpenApi.spec()
end
defp generate_spec_manually do
%OpenApi{
info: %Info{
title: "WandererApp v1 JSON:API",
version: "1.0.0",
description: """
JSON:API compliant endpoints for WandererApp.
## Features
- Filtering: Use `filter[attribute]=value` parameters
- Sorting: Use `sort=attribute` or `sort=-attribute` for descending
- Pagination: Use `page[limit]=n` and `page[offset]=n`
- Relationships: Include related resources with `include=relationship`
## Authentication
All endpoints require Bearer token authentication:
```
Authorization: Bearer YOUR_API_KEY
```
"""
},
servers: [
Server.from_endpoint(WandererAppWeb.Endpoint)
],
paths: get_v1_paths(),
components: %Components{
schemas: get_v1_schemas(),
securitySchemes: %{
"bearerAuth" => %{
"type" => "http",
"scheme" => "bearer",
"description" => "Map API key for authentication"
}
}
},
security: [%{"bearerAuth" => []}],
tags: get_v1_tags()
}
end
defp get_v1_tags do
[
%{"name" => "Access Lists", "description" => "Access control list management"},
%{"name" => "Access List Members", "description" => "ACL member management"},
%{"name" => "Characters", "description" => "Character management"},
%{"name" => "Maps", "description" => "Map management"},
%{"name" => "Map Systems", "description" => "Map system operations"},
%{"name" => "Map Connections", "description" => "System connection management"},
%{"name" => "Map Solar Systems", "description" => "Solar system data"},
%{"name" => "Map System Signatures", "description" => "Wormhole signature tracking"},
%{"name" => "Map System Structures", "description" => "Structure management"},
%{"name" => "Map System Comments", "description" => "System comments"},
%{"name" => "Map Character Settings", "description" => "Character map settings"},
%{"name" => "Map User Settings", "description" => "User map preferences"},
%{"name" => "Map Subscriptions", "description" => "Map subscription management"},
%{"name" => "Map Access Lists", "description" => "Map-specific ACLs"},
%{"name" => "Map States", "description" => "Map state information"},
%{"name" => "Users", "description" => "User management"},
%{"name" => "User Activities", "description" => "User activity tracking"},
%{"name" => "Ship Type Info", "description" => "Ship type information"}
]
end
defp get_v1_paths do
# Generate paths for all resources
resources = [
{"access_lists", "Access Lists"},
{"access_list_members", "Access List Members"},
{"characters", "Characters"},
{"maps", "Maps"},
{"map_systems", "Map Systems"},
{"map_connections", "Map Connections"},
{"map_solar_systems", "Map Solar Systems"},
{"map_system_signatures", "Map System Signatures"},
{"map_system_structures", "Map System Structures"},
{"map_system_comments", "Map System Comments"},
{"map_character_settings", "Map Character Settings"},
{"map_user_settings", "Map User Settings"},
{"map_subscriptions", "Map Subscriptions"},
{"map_access_lists", "Map Access Lists"},
{"map_states", "Map States"},
{"users", "Users"},
{"user_activities", "User Activities"},
{"ship_type_infos", "Ship Type Info"}
]
Enum.reduce(resources, %{}, fn {resource, tag}, acc ->
base_path = "/api/v1/#{resource}"
paths = %{
base_path => %{
"get" => %{
"summary" => "List #{resource}",
"tags" => [tag],
"parameters" => get_standard_list_parameters(resource),
"responses" => %{
"200" => %{
"description" => "List of #{resource}",
"content" => %{
"application/vnd.api+json" => %{
"schema" => %{
"$ref" => "#/components/schemas/#{String.capitalize(resource)}ListResponse"
}
}
}
}
}
},
"post" => %{
"summary" => "Create #{String.replace(resource, "_", " ")}",
"tags" => [tag],
"requestBody" => %{
"required" => true,
"content" => %{
"application/vnd.api+json" => %{
"schema" => %{
"$ref" => "#/components/schemas/#{String.capitalize(resource)}CreateRequest"
}
}
}
},
"responses" => %{
"201" => %{"description" => "Created"}
}
}
},
"#{base_path}/{id}" => %{
"get" => %{
"summary" => "Get #{String.replace(resource, "_", " ")}",
"tags" => [tag],
"parameters" => [
%{
"name" => "id",
"in" => "path",
"required" => true,
"schema" => %{"type" => "string"}
}
],
"responses" => %{
"200" => %{"description" => "Resource details"}
}
},
"patch" => %{
"summary" => "Update #{String.replace(resource, "_", " ")}",
"tags" => [tag],
"parameters" => [
%{
"name" => "id",
"in" => "path",
"required" => true,
"schema" => %{"type" => "string"}
}
],
"requestBody" => %{
"required" => true,
"content" => %{
"application/vnd.api+json" => %{
"schema" => %{
"$ref" => "#/components/schemas/#{String.capitalize(resource)}UpdateRequest"
}
}
}
},
"responses" => %{
"200" => %{"description" => "Updated"}
}
},
"delete" => %{
"summary" => "Delete #{String.replace(resource, "_", " ")}",
"tags" => [tag],
"parameters" => [
%{
"name" => "id",
"in" => "path",
"required" => true,
"schema" => %{"type" => "string"}
}
],
"responses" => %{
"204" => %{"description" => "Deleted"}
}
}
}
}
Map.merge(acc, paths)
end)
|> add_custom_paths()
end
defp add_custom_paths(paths) do
# Add custom action paths
custom_paths = %{
"/api/v1/maps/{id}/duplicate" => %{
"post" => %{
"summary" => "Duplicate map",
"tags" => ["Maps"],
"parameters" => [
%{
"name" => "id",
"in" => "path",
"required" => true,
"schema" => %{"type" => "string"}
}
],
"responses" => %{
"201" => %{"description" => "Map duplicated"}
}
}
},
"/api/v1/maps/{map_id}/systems_and_connections" => %{
"get" => %{
"summary" => "Get Map Systems and Connections",
"description" => "Retrieve both systems and connections for a map in a single response",
"tags" => ["Maps"],
"parameters" => [
%{
"name" => "map_id",
"in" => "path",
"required" => true,
"schema" => %{"type" => "string"},
"description" => "Map ID"
}
],
"responses" => %{
"200" => %{
"description" => "Combined systems and connections data",
"content" => %{
"application/json" => %{
"schema" => %{
"type" => "object",
"properties" => %{
"systems" => %{
"type" => "array",
"items" => %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string"},
"solar_system_id" => %{"type" => "integer"},
"name" => %{"type" => "string"},
"status" => %{"type" => "string"},
"visible" => %{"type" => "boolean"},
"locked" => %{"type" => "boolean"},
"position_x" => %{"type" => "integer"},
"position_y" => %{"type" => "integer"}
}
}
},
"connections" => %{
"type" => "array",
"items" => %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string"},
"solar_system_source" => %{"type" => "integer"},
"solar_system_target" => %{"type" => "integer"},
"type" => %{"type" => "string"},
"time_status" => %{"type" => "string"},
"mass_status" => %{"type" => "string"}
}
}
}
}
}
}
}
},
"404" => %{"description" => "Map not found"},
"401" => %{"description" => "Unauthorized"}
}
}
}
}
Map.merge(paths, custom_paths)
end
defp get_standard_list_parameters(resource) do
base_params = [
%{
"name" => "sort",
"in" => "query",
"description" => "Sort results (e.g., 'name', '-created_at')",
"schema" => %{"type" => "string"}
},
%{
"name" => "page[limit]",
"in" => "query",
"description" => "Number of results per page",
"schema" => %{"type" => "integer", "default" => 50}
},
%{
"name" => "page[offset]",
"in" => "query",
"description" => "Offset for pagination",
"schema" => %{"type" => "integer", "default" => 0}
},
%{
"name" => "include",
"in" => "query",
"description" => "Include related resources (comma-separated)",
"schema" => %{"type" => "string"}
}
]
# Add resource-specific filter parameters
filter_params =
case resource do
"characters" ->
[
%{
"name" => "filter[name]",
"in" => "query",
"description" => "Filter by character name",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[user_id]",
"in" => "query",
"description" => "Filter by user ID",
"schema" => %{"type" => "string"}
}
]
"maps" ->
[
%{
"name" => "filter[scope]",
"in" => "query",
"description" => "Filter by map scope",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[archived]",
"in" => "query",
"description" => "Filter by archived status",
"schema" => %{"type" => "boolean"}
}
]
"map_systems" ->
[
%{
"name" => "filter[map_id]",
"in" => "query",
"description" => "Filter by map ID",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[solar_system_id]",
"in" => "query",
"description" => "Filter by solar system ID",
"schema" => %{"type" => "integer"}
}
]
"map_connections" ->
[
%{
"name" => "filter[map_id]",
"in" => "query",
"description" => "Filter by map ID",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[source_id]",
"in" => "query",
"description" => "Filter by source system ID",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[target_id]",
"in" => "query",
"description" => "Filter by target system ID",
"schema" => %{"type" => "string"}
}
]
"map_system_signatures" ->
[
%{
"name" => "filter[system_id]",
"in" => "query",
"description" => "Filter by system ID",
"schema" => %{"type" => "string"}
},
%{
"name" => "filter[type]",
"in" => "query",
"description" => "Filter by signature type",
"schema" => %{"type" => "string"}
}
]
_ ->
[]
end
base_params ++ filter_params
end
defp get_v1_schemas do
%{
# Generic JSON:API response wrapper
"JsonApiWrapper" => %{
"type" => "object",
"properties" => %{
"data" => %{
"type" => "object",
"description" => "Primary data"
},
"included" => %{
"type" => "array",
"description" => "Included related resources"
},
"meta" => %{
"type" => "object",
"description" => "Metadata about the response"
},
"links" => %{
"type" => "object",
"description" => "Links for pagination and relationships"
}
}
},
# Character schemas
"CharacterResource" => %{
"type" => "object",
"properties" => %{
"type" => %{"type" => "string", "enum" => ["characters"]},
"id" => %{"type" => "string"},
"attributes" => %{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string"},
"eve_id" => %{"type" => "integer"},
"corporation_id" => %{"type" => "integer"},
"alliance_id" => %{"type" => "integer"},
"online" => %{"type" => "boolean"},
"location" => %{"type" => "object"},
"inserted_at" => %{"type" => "string", "format" => "date-time"},
"updated_at" => %{"type" => "string", "format" => "date-time"}
}
},
"relationships" => %{
"type" => "object",
"properties" => %{
"user" => %{
"type" => "object",
"properties" => %{
"data" => %{
"type" => "object",
"properties" => %{
"type" => %{"type" => "string"},
"id" => %{"type" => "string"}
}
}
}
}
}
}
}
},
"CharactersListResponse" => %{
"type" => "object",
"properties" => %{
"data" => %{
"type" => "array",
"items" => %{"$ref" => "#/components/schemas/CharacterResource"}
},
"meta" => %{
"type" => "object",
"properties" => %{
"page" => %{
"type" => "object",
"properties" => %{
"offset" => %{"type" => "integer"},
"limit" => %{"type" => "integer"},
"total" => %{"type" => "integer"}
}
}
}
}
}
},
# Map schemas
"MapResource" => %{
"type" => "object",
"properties" => %{
"type" => %{"type" => "string", "enum" => ["maps"]},
"id" => %{"type" => "string"},
"attributes" => %{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string"},
"slug" => %{"type" => "string"},
"scope" => %{"type" => "string"},
"public_key" => %{"type" => "string"},
"archived" => %{"type" => "boolean"},
"inserted_at" => %{"type" => "string", "format" => "date-time"},
"updated_at" => %{"type" => "string", "format" => "date-time"}
}
},
"relationships" => %{
"type" => "object",
"properties" => %{
"owner" => %{
"type" => "object"
},
"characters" => %{
"type" => "object"
},
"acls" => %{
"type" => "object"
}
}
}
}
}
}
end
end

View File

@@ -234,7 +234,6 @@ defmodule WandererAppWeb.Router do
plug WandererAppWeb.Plugs.CheckApiDisabled
plug WandererAppWeb.Plugs.JsonApiPerformanceMonitor
plug WandererAppWeb.Plugs.CheckJsonApiAuth
plug WandererAppWeb.Plugs.ConditionalAssignMapOwner
# Future: Add rate limiting, advanced permissions, etc.
end
@@ -598,7 +597,7 @@ defmodule WandererAppWeb.Router do
scope "/api/v1" do
pipe_through :api_v1
# Custom combined endpoints
# Custom combined endpoint with map_id in path
get "/maps/:map_id/systems_and_connections",
WandererAppWeb.Api.MapSystemsConnectionsController,
:show
@@ -606,6 +605,18 @@ defmodule WandererAppWeb.Router do
# Forward all v1 requests to AshJsonApi router
# This will automatically generate RESTful JSON:API endpoints
# for all Ash resources once they're configured with the AshJsonApi extension
#
# NOTE: AshJsonApi generates flat routes (e.g., /api/v1/map_systems)
# Phoenix's `forward` cannot be used with dynamic path segments, so proper
# nested routes like /api/v1/maps/{id}/systems would require custom controllers.
#
# Current approach: Use flat routes with map_id in request body or filters:
# - POST /api/v1/map_systems with {"data": {"attributes": {"map_id": "..."}}}
# - GET /api/v1/map_systems?filter[map_id]=...
# - PATCH /api/v1/map_systems/{id} with map_id in body
#
# Authentication is handled by CheckJsonApiAuth which validates the Bearer
# token against the map's API key.
forward "/", WandererAppWeb.ApiV1Router
end
end

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.84.9"
@version "1.84.13"
def project do
[

View File

@@ -144,33 +144,28 @@ The API v1 provides access to over 25 resources through the Ash Framework. Here
### Core Resources
- **Maps** (`/api/v1/maps`) - Map management with full CRUD operations
- **Characters** (`/api/v1/characters`) - Character tracking and management (GET, DELETE only)
- **Access Lists** (`/api/v1/access_lists`) - ACL management and permissions
- **Access List Members** (`/api/v1/access_list_members`) - ACL member management
- **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
### Map Resources
- **Map Systems** (`/api/v1/map_systems`) - Solar system data and metadata
- **Map Connections** (`/api/v1/map_connections`) - Wormhole connections
- **Map Signatures** (`/api/v1/map_system_signatures`) - Signature scanning data (GET, DELETE only)
- **Map Structures** (`/api/v1/map_system_structures`) - Structure information
- **Map Subscriptions** (`/api/v1/map_subscriptions`) - Subscription management (GET only)
- **Map Systems and Connections** (`/api/v1/maps/{map_id}/systems_and_connections`) - Combined endpoint (GET only)
- **Map Systems** (`/api/v1/map_systems`) - Solar system data and metadata with full CRUD operations (paginated: default 100, max 500)
- **Map Connections** (`/api/v1/map_connections`) - Wormhole connections with full CRUD operations
- **Map Signatures** (`/api/v1/map_system_signatures`) - Signature scanning data (read and delete only, paginated: default 50, max 200)
- **Map Structures** (`/api/v1/map_system_structures`) - Structure information with full CRUD operations
- **Map Subscriptions** (`/api/v1/map_subscriptions`) - Subscription management (read-only)
- **Map Default Settings** (`/api/v1/map_default_settings`) - Default map configurations with full CRUD operations
- **Map Systems and Connections** (`/api/v1/maps/{map_id}/systems_and_connections`) - Combined endpoint (read-only)
### System Resources
- **Map System Comments** (`/api/v1/map_system_comments`) - System annotations (GET only)
- **Map System Comments** (`/api/v1/map_system_comments`) - System annotations (read-only)
### User Resources
- **User Activities** (`/api/v1/user_activities`) - User activity tracking (GET only)
- **Map Character Settings** (`/api/v1/map_character_settings`) - Character preferences (GET only)
- **Map User Settings** (`/api/v1/map_user_settings`) - User map preferences (GET only)
- **User Activities** (`/api/v1/user_activities`) - User activity tracking (read-only, paginated: default 15)
- **Map Character Settings** (`/api/v1/map_character_settings`) - Character preferences (read-only)
- **Map User Settings** (`/api/v1/map_user_settings`) - User map preferences (read-only)
### Additional Resources
- **Map Webhook Subscriptions** (`/api/v1/map_webhook_subscriptions`) - Webhook management
- **Map Invites** (`/api/v1/map_invites`) - Map invitation system
- **Map Pings** (`/api/v1/map_pings`) - In-game ping tracking
- **Corp Wallet Transactions** (`/api/v1/corp_wallet_transactions`) - Corporation finances
*Note: Some resources have been restricted to read-only access for security and consistency. Resources marked as "(GET only)" support only read operations, while "(GET, DELETE only)" support read and delete operations.*
*Note: Resources marked as "full CRUD operations" support create, read, update, and delete. Resources marked as "read-only" support only GET operations. Resources marked as "read and delete only" support GET and DELETE operations. Pagination limits are configurable via `page[limit]` and `page[offset]` parameters where supported.*
## API v1 Feature Set

View File

@@ -2,4 +2,15 @@
export ERL_AFLAGS="-proto_dist inet6_tcp"
export RELEASE_DISTRIBUTION="name"
export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
# Use custom RELEASE_NODE if set, otherwise detect environment
if [ -n "$RELEASE_NODE" ]; then
# RELEASE_NODE already set, use as-is
export RELEASE_NODE
elif [ -n "$FLY_APP_NAME" ] && [ -n "$FLY_IMAGE_REF" ] && [ -n "$FLY_PRIVATE_IP" ]; then
# Fly.io environment detected
export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
else
# Generic deployment - use hostname
export RELEASE_NODE="wanderer@$(hostname)"
fi

View File

@@ -0,0 +1,18 @@
# Example environment file for manual API tests
# Copy this to .env and fill in your values
# Your Wanderer server URL
API_BASE_URL=http://localhost:8000
# Your map's slug (found in the map URL: /your-map-slug)
MAP_SLUG=your-map-slug
# Your map's public API token (found in map settings)
API_TOKEN=your_map_public_api_key_here
# For character_eve_id testing:
# Find a valid character EVE ID from your database
VALID_CHAR_ID=111111111
# Use any non-existent character ID for invalid tests
INVALID_CHAR_ID=999999999

View File

@@ -0,0 +1,249 @@
# Manual cURL Testing for Character EVE ID Fix (Issue #539)
This guide provides standalone curl commands to manually test the character_eve_id fix.
## Prerequisites
1. **Get your Map's Public API Token:**
- Log into Wanderer
- Go to your map settings
- Find the "Public API Key" section
- Copy your API token
2. **Find your Map Slug:**
- Look at your map URL: `https://your-instance.com/your-map-slug`
- The slug is the last part of the URL
3. **Get a valid Character EVE ID:**
```bash
# Option 1: Query your database
psql $DATABASE_URL -c "SELECT eve_id, name FROM character_v1 WHERE deleted = false LIMIT 5;"
# Option 2: Use the characters API
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
http://localhost:8000/api/characters
```
4. **Get a Solar System ID from your map:**
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
http://localhost:8000/api/maps/YOUR_SLUG/systems \
| jq '.data[0].solar_system_id'
```
## Set Environment Variables (for convenience)
```bash
export API_BASE_URL="http://localhost:8000"
export MAP_SLUG="your-map-slug"
export API_TOKEN="your_api_token_here"
export SOLAR_SYSTEM_ID="30000142" # Replace with actual system ID from your map
export VALID_CHAR_ID="111111111" # Replace with real character eve_id
export INVALID_CHAR_ID="999999999" # Non-existent character
```
---
## Test 1: Create Signature with Valid character_eve_id
**Expected Result:** HTTP 201, returned object has the submitted character_eve_id
```bash
curl -v -X POST \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solar_system_id": '"$SOLAR_SYSTEM_ID"',
"eve_id": "TEST-001",
"character_eve_id": "'"$VALID_CHAR_ID"'",
"group": "wormhole",
"kind": "cosmic_signature",
"name": "Test Signature 1"
}' \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures" | jq '.'
```
**Verification:**
```bash
# The response should contain:
# "character_eve_id": "111111111" (your VALID_CHAR_ID)
```
---
## Test 2: Create Signature with Invalid character_eve_id
**Expected Result:** HTTP 422 with error "invalid_character"
```bash
curl -v -X POST \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solar_system_id": '"$SOLAR_SYSTEM_ID"',
"eve_id": "TEST-002",
"character_eve_id": "'"$INVALID_CHAR_ID"'",
"group": "wormhole",
"kind": "cosmic_signature"
}' \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures" | jq '.'
```
**Expected Response:**
```json
{
"error": "invalid_character"
}
```
---
## Test 3: Create Signature WITHOUT character_eve_id (Backward Compatibility)
**Expected Result:** HTTP 201, uses map owner's character_eve_id as fallback
```bash
curl -v -X POST \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solar_system_id": '"$SOLAR_SYSTEM_ID"',
"eve_id": "TEST-003",
"group": "data",
"kind": "cosmic_signature",
"name": "Test Signature 3"
}' \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures" | jq '.'
```
**Verification:**
```bash
# The response should contain the map owner's character_eve_id
# This proves backward compatibility is maintained
```
---
## Test 4: Update Signature with Valid character_eve_id
**Expected Result:** HTTP 200, returned object has the submitted character_eve_id
```bash
# First, save a signature ID from Test 1 or 3
export SIG_ID="paste-signature-id-here"
curl -v -X PUT \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Signature Name",
"character_eve_id": "'"$VALID_CHAR_ID"'",
"description": "Updated via API"
}' \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$SIG_ID" | jq '.'
```
**Verification:**
```bash
# The response should contain:
# "character_eve_id": "111111111" (your VALID_CHAR_ID)
```
---
## Test 5: Update Signature with Invalid character_eve_id
**Expected Result:** HTTP 422 with error "invalid_character"
```bash
curl -v -X PUT \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Should Fail",
"character_eve_id": "'"$INVALID_CHAR_ID"'"
}' \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$SIG_ID" | jq '.'
```
**Expected Response:**
```json
{
"error": "invalid_character"
}
```
---
## Cleanup
Delete test signatures:
```bash
# List all signatures to find IDs
curl -H "Authorization: Bearer $API_TOKEN" \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures" | jq '.data[] | {id, eve_id, name}'
# Delete specific signature
export SIG_ID="signature-uuid-here"
curl -v -X DELETE \
-H "Authorization: Bearer $API_TOKEN" \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$SIG_ID"
```
---
## Quick Debugging Tips
### View All Signatures
```bash
curl -H "Authorization: Bearer $API_TOKEN" \
"$API_BASE_URL/api/maps/$MAP_SLUG/signatures" \
| jq '.data[] | {id, eve_id, character_eve_id, name}'
```
### View All Characters in Database
```bash
curl -H "Authorization: Bearer $API_TOKEN" \
"$API_BASE_URL/api/characters" \
| jq '.[] | {eve_id, name}'
```
### View All Systems in Map
```bash
curl -H "Authorization: Bearer $API_TOKEN" \
"$API_BASE_URL/api/maps/$MAP_SLUG/systems" \
| jq '.data[] | {id, solar_system_id, name}'
```
---
## Expected Behavior Summary
| Test Case | HTTP Status | character_eve_id in Response |
|-----------|-------------|------------------------------|
| Create with valid char ID | 201 | Matches submitted value |
| Create with invalid char ID | 422 | N/A (error returned) |
| Create without char ID | 201 | Map owner's char ID (fallback) |
| Update with valid char ID | 200 | Matches submitted value |
| Update with invalid char ID | 422 | N/A (error returned) |
---
## Troubleshooting
### "Unauthorized (invalid token for map)"
- Double-check your API_TOKEN matches the map's public API key
- Verify the token doesn't have extra spaces or newlines
### "Map not found"
- Verify your MAP_SLUG is correct
- Try using the map UUID instead of slug
### "System not found for solar_system_id"
- The system must already exist in your map
- Run the "View All Systems" command to find valid system IDs
### "invalid_character" when using what should be valid
- Verify the character exists: `SELECT * FROM character_v1 WHERE eve_id = 'YOUR_ID';`
- Make sure `deleted = false` for the character

View File

@@ -0,0 +1,289 @@
#!/bin/bash
# test/manual/api/test_character_eve_id_fix.sh
# ─── Manual Test for Character EVE ID Fix (Issue #539) ────────────────────────
#
# This script tests the fix for GitHub issue #539 where character_eve_id
# was being ignored when creating/updating signatures via the REST API.
#
# Usage:
# 1. Create a .env file in this directory with:
# API_TOKEN=your_map_public_api_key
# API_BASE_URL=http://localhost:8000 # or your server URL
# MAP_SLUG=your_map_slug
# VALID_CHAR_ID=111111111 # A character that exists in your database
# INVALID_CHAR_ID=999999999 # A character that does NOT exist
#
# 2. Run: ./test_character_eve_id_fix.sh
#
# Prerequisites:
# - curl and jq must be installed
# - A map must exist with a valid API token
# - At least one system must be added to the map
set -eu
source "$(dirname "$0")/utils.sh"
echo "═══════════════════════════════════════════════════════════════════"
echo "Testing Character EVE ID Fix (GitHub Issue #539)"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
# Check required environment variables
: "${API_BASE_URL:?Error: API_BASE_URL not set}"
: "${MAP_SLUG:?Error: MAP_SLUG not set}"
: "${VALID_CHAR_ID:?Error: VALID_CHAR_ID not set (provide a character eve_id that exists in DB)}"
: "${INVALID_CHAR_ID:?Error: INVALID_CHAR_ID not set (provide a non-existent character eve_id)}"
# Get a system to use for testing
echo "📋 Fetching available systems from map..."
SYSTEMS_RAW=$(make_request GET "$API_BASE_URL/api/maps/$MAP_SLUG/systems")
SYSTEMS_STATUS=$(parse_status "$SYSTEMS_RAW")
SYSTEMS_RESPONSE=$(parse_response "$SYSTEMS_RAW")
if [ "$SYSTEMS_STATUS" != "200" ]; then
echo "❌ Failed to fetch systems (HTTP $SYSTEMS_STATUS)"
echo "$SYSTEMS_RESPONSE"
exit 1
fi
# Extract first system's solar_system_id
SOLAR_SYSTEM_ID=$(echo "$SYSTEMS_RESPONSE" | jq -r '.data[0].solar_system_id // empty')
if [ -z "$SOLAR_SYSTEM_ID" ]; then
echo "❌ No systems found in map. Please add at least one system first."
exit 1
fi
echo "✅ Using solar_system_id: $SOLAR_SYSTEM_ID"
echo ""
# ═══════════════════════════════════════════════════════════════════════
# Test 1: Create signature with valid character_eve_id
# ═══════════════════════════════════════════════════════════════════════
echo "─────────────────────────────────────────────────────────────────"
echo "Test 1: Create signature with VALID character_eve_id"
echo "─────────────────────────────────────────────────────────────────"
PAYLOAD1=$(cat <<EOF
{
"solar_system_id": $SOLAR_SYSTEM_ID,
"eve_id": "TEST-001",
"character_eve_id": "$VALID_CHAR_ID",
"group": "wormhole",
"kind": "cosmic_signature",
"name": "Test Sig 1"
}
EOF
)
echo "Request:"
echo "$PAYLOAD1" | jq '.'
echo ""
RAW1=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/signatures" "$PAYLOAD1")
STATUS1=$(parse_status "$RAW1")
RESPONSE1=$(parse_response "$RAW1")
echo "Response (HTTP $STATUS1):"
echo "$RESPONSE1" | jq '.'
echo ""
if [ "$STATUS1" = "201" ]; then
RETURNED_CHAR_ID=$(echo "$RESPONSE1" | jq -r '.data.character_eve_id')
if [ "$RETURNED_CHAR_ID" = "$VALID_CHAR_ID" ]; then
echo "✅ PASS: Signature created with correct character_eve_id: $RETURNED_CHAR_ID"
SIG_ID_1=$(echo "$RESPONSE1" | jq -r '.data.id')
else
echo "❌ FAIL: Expected character_eve_id=$VALID_CHAR_ID, got $RETURNED_CHAR_ID"
fi
else
echo "❌ FAIL: Expected HTTP 201, got $STATUS1"
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════
# Test 2: Create signature with invalid character_eve_id
# ═══════════════════════════════════════════════════════════════════════
echo "─────────────────────────────────────────────────────────────────"
echo "Test 2: Create signature with INVALID character_eve_id"
echo "─────────────────────────────────────────────────────────────────"
PAYLOAD2=$(cat <<EOF
{
"solar_system_id": $SOLAR_SYSTEM_ID,
"eve_id": "TEST-002",
"character_eve_id": "$INVALID_CHAR_ID",
"group": "wormhole",
"kind": "cosmic_signature"
}
EOF
)
echo "Request:"
echo "$PAYLOAD2" | jq '.'
echo ""
RAW2=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/signatures" "$PAYLOAD2")
STATUS2=$(parse_status "$RAW2")
RESPONSE2=$(parse_response "$RAW2")
echo "Response (HTTP $STATUS2):"
echo "$RESPONSE2" | jq '.'
echo ""
if [ "$STATUS2" = "422" ]; then
ERROR_MSG=$(echo "$RESPONSE2" | jq -r '.error // empty')
if [ "$ERROR_MSG" = "invalid_character" ]; then
echo "✅ PASS: Correctly rejected invalid character_eve_id with error: $ERROR_MSG"
else
echo "⚠️ PARTIAL: Got HTTP 422 but unexpected error message: $ERROR_MSG"
fi
else
echo "❌ FAIL: Expected HTTP 422, got $STATUS2"
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════
# Test 3: Create signature WITHOUT character_eve_id (fallback test)
# ═══════════════════════════════════════════════════════════════════════
echo "─────────────────────────────────────────────────────────────────"
echo "Test 3: Create signature WITHOUT character_eve_id (fallback)"
echo "─────────────────────────────────────────────────────────────────"
PAYLOAD3=$(cat <<EOF
{
"solar_system_id": $SOLAR_SYSTEM_ID,
"eve_id": "TEST-003",
"group": "data",
"kind": "cosmic_signature",
"name": "Test Sig 3"
}
EOF
)
echo "Request:"
echo "$PAYLOAD3" | jq '.'
echo ""
RAW3=$(make_request POST "$API_BASE_URL/api/maps/$MAP_SLUG/signatures" "$PAYLOAD3")
STATUS3=$(parse_status "$RAW3")
RESPONSE3=$(parse_response "$RAW3")
echo "Response (HTTP $STATUS3):"
echo "$RESPONSE3" | jq '.'
echo ""
if [ "$STATUS3" = "201" ]; then
RETURNED_CHAR_ID=$(echo "$RESPONSE3" | jq -r '.data.character_eve_id')
echo "✅ PASS: Signature created with fallback character_eve_id: $RETURNED_CHAR_ID"
echo " (This should be the map owner's character)"
SIG_ID_3=$(echo "$RESPONSE3" | jq -r '.data.id')
else
echo "❌ FAIL: Expected HTTP 201, got $STATUS3"
fi
echo ""
# ═══════════════════════════════════════════════════════════════════════
# Test 4: Update signature with valid character_eve_id
# ═══════════════════════════════════════════════════════════════════════
if [ -n "${SIG_ID_1:-}" ]; then
echo "─────────────────────────────────────────────────────────────────"
echo "Test 4: Update signature with VALID character_eve_id"
echo "─────────────────────────────────────────────────────────────────"
PAYLOAD4=$(cat <<EOF
{
"name": "Updated Test Sig 1",
"character_eve_id": "$VALID_CHAR_ID",
"description": "Updated via API"
}
EOF
)
echo "Request:"
echo "$PAYLOAD4" | jq '.'
echo ""
RAW4=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$SIG_ID_1" "$PAYLOAD4")
STATUS4=$(parse_status "$RAW4")
RESPONSE4=$(parse_response "$RAW4")
echo "Response (HTTP $STATUS4):"
echo "$RESPONSE4" | jq '.'
echo ""
if [ "$STATUS4" = "200" ]; then
RETURNED_CHAR_ID=$(echo "$RESPONSE4" | jq -r '.data.character_eve_id')
if [ "$RETURNED_CHAR_ID" = "$VALID_CHAR_ID" ]; then
echo "✅ PASS: Signature updated with correct character_eve_id: $RETURNED_CHAR_ID"
else
echo "❌ FAIL: Expected character_eve_id=$VALID_CHAR_ID, got $RETURNED_CHAR_ID"
fi
else
echo "❌ FAIL: Expected HTTP 200, got $STATUS4"
fi
echo ""
fi
# ═══════════════════════════════════════════════════════════════════════
# Test 5: Update signature with invalid character_eve_id
# ═══════════════════════════════════════════════════════════════════════
if [ -n "${SIG_ID_3:-}" ]; then
echo "─────────────────────────────────────────────────────────────────"
echo "Test 5: Update signature with INVALID character_eve_id"
echo "─────────────────────────────────────────────────────────────────"
PAYLOAD5=$(cat <<EOF
{
"name": "Should Fail",
"character_eve_id": "$INVALID_CHAR_ID"
}
EOF
)
echo "Request:"
echo "$PAYLOAD5" | jq '.'
echo ""
RAW5=$(make_request PUT "$API_BASE_URL/api/maps/$MAP_SLUG/signatures/$SIG_ID_3" "$PAYLOAD5")
STATUS5=$(parse_status "$RAW5")
RESPONSE5=$(parse_response "$RAW5")
echo "Response (HTTP $STATUS5):"
echo "$RESPONSE5" | jq '.'
echo ""
if [ "$STATUS5" = "422" ]; then
ERROR_MSG=$(echo "$RESPONSE5" | jq -r '.error // empty')
if [ "$ERROR_MSG" = "invalid_character" ]; then
echo "✅ PASS: Correctly rejected invalid character_eve_id with error: $ERROR_MSG"
else
echo "⚠️ PARTIAL: Got HTTP 422 but unexpected error message: $ERROR_MSG"
fi
else
echo "❌ FAIL: Expected HTTP 422, got $STATUS5"
fi
echo ""
fi
# ═══════════════════════════════════════════════════════════════════════
# Cleanup (optional)
# ═══════════════════════════════════════════════════════════════════════
echo "─────────────────────────────────────────────────────────────────"
echo "Cleanup"
echo "─────────────────────────────────────────────────────────────────"
echo "Created signature IDs: ${SIG_ID_1:-none} ${SIG_ID_3:-none}"
echo ""
echo "To clean up manually, delete these signatures via the UI or API:"
for sig_id in ${SIG_ID_1:-} ${SIG_ID_3:-}; do
if [ -n "$sig_id" ]; then
echo " curl -X DELETE -H 'Authorization: Bearer \$API_TOKEN' \\"
echo " $API_BASE_URL/api/maps/$MAP_SLUG/signatures/$sig_id"
fi
done
echo ""
echo "═══════════════════════════════════════════════════════════════════"
echo "Test Complete!"
echo "═══════════════════════════════════════════════════════════════════"