mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-22 17:16:18 +00:00
Compare commits
51 Commits
refactor-m
...
fix-error-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c54a3413 | ||
|
|
0439110938 | ||
|
|
8ce1e5fa3e | ||
|
|
ebaf6bcdc6 | ||
|
|
40d947bebc | ||
|
|
61d1c3848f | ||
|
|
e152ce179f | ||
|
|
7bbe387183 | ||
|
|
b1555ff03c | ||
|
|
e624499244 | ||
|
|
6a1976dec6 | ||
|
|
3db24c4344 | ||
|
|
883c09f255 | ||
|
|
ff24d80038 | ||
|
|
63cbc9c0b9 | ||
|
|
8056972a27 | ||
|
|
1759d46740 | ||
|
|
e4b7d2e45b | ||
|
|
41573cbee3 | ||
|
|
24ffc20bb8 | ||
|
|
e077849b66 | ||
|
|
375a9ef65b | ||
|
|
9bf90ab752 | ||
|
|
90c3481151 | ||
|
|
e36b08a7e5 | ||
|
|
e1f79170c3 | ||
|
|
68b5455e91 | ||
|
|
f28e75c7f4 | ||
|
|
6091adb28e | ||
|
|
d4657b335f | ||
|
|
7fee850902 | ||
|
|
648c168a66 | ||
|
|
f5c4b2c407 | ||
|
|
b592223d52 | ||
|
|
5cf118c6ee | ||
|
|
b25013c652 | ||
|
|
cf43861b11 | ||
|
|
b5fe8f8878 | ||
|
|
5e5068c7de | ||
|
|
624b51edfb | ||
|
|
a72f8e60c4 | ||
|
|
dec8ae50c9 | ||
|
|
0332d36a8e | ||
|
|
8444c7f82d | ||
|
|
ec3fc7447e | ||
|
|
20ec2800c9 | ||
|
|
6fbf43e860 | ||
|
|
697da38020 | ||
|
|
4bc65b43d2 | ||
|
|
910ec97fd1 | ||
|
|
40ed58ee8c |
@@ -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>"
|
||||
|
||||
98
CHANGELOG.md
98
CHANGELOG.md
@@ -2,6 +2,104 @@
|
||||
|
||||
<!-- 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)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.8](https://github.com/wanderer-industries/wanderer/compare/v1.84.7...v1.84.8) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added cleanup jobs for old system signatures & chain passages
|
||||
|
||||
## [v1.84.7](https://github.com/wanderer-industries/wanderer/compare/v1.84.6...v1.84.7) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* api and structure search fixes
|
||||
|
||||
## [v1.84.6](https://github.com/wanderer-industries/wanderer/compare/v1.84.5...v1.84.6) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Added map slug uniqness checking while using API
|
||||
|
||||
## [v1.84.5](https://github.com/wanderer-industries/wanderer/compare/v1.84.4...v1.84.5) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Added tracking for map & character event handling errors
|
||||
|
||||
## [v1.84.4](https://github.com/wanderer-industries/wanderer/compare/v1.84.3...v1.84.4) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed issue with updating system signatures
|
||||
|
||||
## [v1.84.3](https://github.com/wanderer-industries/wanderer/compare/v1.84.2...v1.84.3) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed linked signature time status update
|
||||
|
||||
## [v1.84.2](https://github.com/wanderer-industries/wanderer/compare/v1.84.1...v1.84.2) (2025-11-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* api: fixed api for get/update map systems
|
||||
|
||||
* add index for map/systems api
|
||||
|
||||
## [v1.84.1](https://github.com/wanderer-industries/wanderer/compare/v1.84.0...v1.84.1) (2025-11-01)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,9 +30,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (structure) {
|
||||
setEditData(structure);
|
||||
@@ -46,34 +43,24 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
const query = e.query.trim();
|
||||
if (!query) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
data: { search: query },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch owners:', err);
|
||||
setOwnerSuggestions([]);
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof StructureItem, val: string | Date) => {
|
||||
@@ -122,7 +109,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
if (editData.ownerId) {
|
||||
try {
|
||||
// TODO fix it
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: editData.ownerId },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
assets/static/images/eo_pp.png
Normal file
BIN
assets/static/images/eo_pp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -258,7 +258,9 @@ config :wanderer_app, WandererApp.Scheduler,
|
||||
timezone: :utc,
|
||||
jobs:
|
||||
[
|
||||
{"@daily", {WandererApp.Map.Audit, :archive, []}}
|
||||
{"@daily", {WandererApp.Map.Audit, :archive, []}},
|
||||
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_chain_passages, []}},
|
||||
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_system_signatures, []}}
|
||||
] ++ sheduler_jobs,
|
||||
timeout: :infinity
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule WandererApp.Api.Changes.SlugifyName do
|
||||
use Ash.Resource.Change
|
||||
|
||||
alias Ash.Changeset
|
||||
require Ash.Query
|
||||
|
||||
@impl true
|
||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
@@ -12,10 +13,56 @@ defmodule WandererApp.Api.Changes.SlugifyName do
|
||||
defp maybe_slugify_name(changeset) do
|
||||
case Changeset.get_attribute(changeset, :slug) do
|
||||
slug when is_binary(slug) ->
|
||||
Changeset.force_change_attribute(changeset, :slug, Slug.slugify(slug))
|
||||
base_slug = Slug.slugify(slug)
|
||||
unique_slug = ensure_unique_slug(changeset, base_slug)
|
||||
Changeset.force_change_attribute(changeset, :slug, unique_slug)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_unique_slug(changeset, base_slug) do
|
||||
# Get the current record ID if this is an update operation
|
||||
current_id = Changeset.get_attribute(changeset, :id)
|
||||
|
||||
# Check if the base slug is available
|
||||
if slug_available?(base_slug, current_id) do
|
||||
base_slug
|
||||
else
|
||||
# Find the next available slug with a numeric suffix
|
||||
find_available_slug(base_slug, current_id, 2)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_slug(base_slug, current_id, n) do
|
||||
candidate_slug = "#{base_slug}-#{n}"
|
||||
|
||||
if slug_available?(candidate_slug, current_id) do
|
||||
candidate_slug
|
||||
else
|
||||
find_available_slug(base_slug, current_id, n + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp slug_available?(slug, current_id) do
|
||||
query =
|
||||
WandererApp.Api.Map
|
||||
|> Ash.Query.filter(slug == ^slug)
|
||||
|> then(fn query ->
|
||||
# Exclude the current record if this is an update
|
||||
if current_id do
|
||||
Ash.Query.filter(query, id != ^current_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
end)
|
||||
|> Ash.Query.limit(1)
|
||||
|
||||
case Ash.read(query) do
|
||||
{:ok, []} -> true
|
||||
{:ok, _} -> false
|
||||
{:error, _} -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule WandererApp.Api.Map do
|
||||
delete(:destroy)
|
||||
|
||||
# Custom action for map duplication
|
||||
post(:duplicate, route: "/:id/duplicate")
|
||||
# post(:duplicate, route: "/:id/duplicate")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ defmodule WandererApp.Api.MapConnection do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_chain_v1")
|
||||
|
||||
custom_indexes do
|
||||
# Critical index for list_connections query performance
|
||||
index [:map_id], name: "map_chain_v1_map_id_index"
|
||||
end
|
||||
end
|
||||
|
||||
json_api do
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :all_active do
|
||||
prepare build(sort: [updated_at: :asc])
|
||||
prepare build(sort: [updated_at: :asc], load: [:map])
|
||||
|
||||
filter(expr(status == :active))
|
||||
end
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
defmodule WandererApp.Api.MapSystem do
|
||||
@moduledoc false
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:map_id,
|
||||
:name,
|
||||
:solar_system_id,
|
||||
:position_x,
|
||||
:position_y,
|
||||
:status,
|
||||
:visible,
|
||||
:locked,
|
||||
:custom_name,
|
||||
:description,
|
||||
:tag,
|
||||
:temporary_name,
|
||||
:labels,
|
||||
:added_at,
|
||||
:linked_sig_eve_id
|
||||
]}
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
@@ -9,6 +29,11 @@ defmodule WandererApp.Api.MapSystem do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_system_v1")
|
||||
|
||||
custom_indexes do
|
||||
# Partial index for efficient visible systems query
|
||||
index [:map_id], where: "visible = true", name: "map_system_v1_map_id_visible_index"
|
||||
end
|
||||
end
|
||||
|
||||
json_api do
|
||||
|
||||
@@ -180,6 +180,8 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
[Tracker Pool] update_online => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
|
||||
ErrorTracker.report(e, __STACKTRACE__)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
|
||||
@@ -212,6 +212,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"time_status" => payload["time_status"] || payload[:time_status],
|
||||
"mass_status" => payload["mass_status"] || payload[:mass_status],
|
||||
"ship_size_type" => payload["ship_size_type"] || payload[:ship_size_type],
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -532,15 +532,16 @@ defmodule WandererApp.Map do
|
||||
solar_system_source,
|
||||
solar_system_target
|
||||
) do
|
||||
case map_id
|
||||
|> get_map!()
|
||||
|> Map.get(:connections, Map.new())
|
||||
connections =
|
||||
map_id
|
||||
|> get_map!()
|
||||
|> Map.get(:connections, Map.new())
|
||||
|
||||
case connections
|
||||
|> Map.get("#{solar_system_source}_#{solar_system_target}") do
|
||||
nil ->
|
||||
{:ok,
|
||||
map_id
|
||||
|> get_map!()
|
||||
|> Map.get(:connections, Map.new())
|
||||
connections
|
||||
|> Map.get("#{solar_system_target}_#{solar_system_source}")}
|
||||
|
||||
connection ->
|
||||
|
||||
38
lib/wanderer_app/map/map_garbage_collector.ex
Normal file
38
lib/wanderer_app/map/map_garbage_collector.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule WandererApp.Map.GarbageCollector do
|
||||
@moduledoc """
|
||||
Manager map subscription plans
|
||||
"""
|
||||
|
||||
require Logger
|
||||
require Ash.Query
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@one_week_seconds 7 * 24 * 60 * 60
|
||||
@two_weeks_seconds 14 * 24 * 60 * 60
|
||||
|
||||
def cleanup_chain_passages() do
|
||||
Logger.info("Start cleanup old map chain passages...")
|
||||
|
||||
WandererApp.Api.MapChainPassages
|
||||
|> Ash.Query.filter(updated_at: [less_than: get_cutoff_time(@one_week_seconds)])
|
||||
|> Ash.bulk_destroy!(:destroy, %{}, batch_size: 100)
|
||||
|
||||
@logger.info(fn -> "All map chain passages processed" end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def cleanup_system_signatures() do
|
||||
Logger.info("Start cleanup old map system signatures...")
|
||||
|
||||
WandererApp.Api.MapSystemSignature
|
||||
|> Ash.Query.filter(updated_at: [less_than: get_cutoff_time(@two_weeks_seconds)])
|
||||
|> Ash.bulk_destroy!(:destroy, %{}, batch_size: 100)
|
||||
|
||||
@logger.info(fn -> "All map system signatures processed" end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_cutoff_time(seconds), do: DateTime.utc_now() |> DateTime.add(-seconds, :second)
|
||||
end
|
||||
@@ -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)
|
||||
@@ -310,7 +310,17 @@ defmodule WandererApp.Map.MapPool do
|
||||
end
|
||||
|
||||
def handle_info(event, state) do
|
||||
Server.Impl.handle_event(event)
|
||||
try do
|
||||
Server.Impl.handle_event(event)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Map Pool] handle_info => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
|
||||
ErrorTracker.report(e, __STACKTRACE__)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -300,10 +300,9 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
defp is_expired(subscription) when is_map(subscription),
|
||||
do: DateTime.compare(DateTime.utc_now(), subscription.active_till) == :gt
|
||||
|
||||
defp renew_subscription(%{auto_renew?: true} = subscription) when is_map(subscription) do
|
||||
with {:ok, %{map: map}} <-
|
||||
subscription |> WandererApp.MapSubscriptionRepo.load_relationships([:map]),
|
||||
{:ok, estimated_price, discount} <- estimate_price(subscription, true),
|
||||
defp renew_subscription(%{auto_renew?: true, map: map} = subscription)
|
||||
when is_map(subscription) do
|
||||
with {:ok, estimated_price, discount} <- estimate_price(subscription, true),
|
||||
{:ok, map_balance} <- get_balance(map) do
|
||||
case map_balance >= estimated_price do
|
||||
true ->
|
||||
|
||||
@@ -35,16 +35,14 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|> Task.async_stream(
|
||||
fn map_id ->
|
||||
try do
|
||||
if WandererApp.Map.Server.map_pid(map_id) do
|
||||
# Always update kill counts
|
||||
update_map_kills(map_id)
|
||||
# Always update kill counts
|
||||
update_map_kills(map_id)
|
||||
|
||||
# Update detailed kills for maps with active subscriptions
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
# Update detailed kills for maps with active subscriptions
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
|
||||
if is_subscription_active do
|
||||
update_detailed_map_kills(map_id)
|
||||
end
|
||||
if is_subscription_active do
|
||||
update_detailed_map_kills(map_id)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
|
||||
@@ -231,31 +231,15 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
attrs
|
||||
) do
|
||||
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
|
||||
result <-
|
||||
:ok <-
|
||||
(try do
|
||||
_allowed_keys = [
|
||||
:mass_status,
|
||||
:ship_size_type,
|
||||
:time_status,
|
||||
:type
|
||||
]
|
||||
|
||||
_update_map =
|
||||
attrs
|
||||
|> Enum.filter(fn {k, _v} ->
|
||||
k in ["mass_status", "ship_size_type", "time_status", "type"]
|
||||
end)
|
||||
|> Enum.map(fn {k, v} -> {String.to_atom(k), v} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
res = apply_connection_updates(map_id, conn_struct, attrs, char_id)
|
||||
res
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("[update_connection] Exception: #{inspect(error)}")
|
||||
{:error, :exception}
|
||||
end),
|
||||
:ok <- result do
|
||||
end) do
|
||||
# Since GenServer updates are asynchronous, manually apply updates to the current struct
|
||||
# to return the correct data immediately instead of refetching from potentially stale cache
|
||||
updated_attrs =
|
||||
@@ -374,6 +358,7 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
"ship_size_type" -> maybe_update_ship_size_type(map_id, conn, val)
|
||||
"time_status" -> maybe_update_time_status(map_id, conn, val)
|
||||
"type" -> maybe_update_type(map_id, conn, val)
|
||||
"locked" -> maybe_update_locked(map_id, conn, val)
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
@@ -429,6 +414,16 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
})
|
||||
end
|
||||
|
||||
defp maybe_update_locked(_map_id, _conn, nil), do: :ok
|
||||
|
||||
defp maybe_update_locked(map_id, conn, value) do
|
||||
Server.update_connection_locked(map_id, %{
|
||||
solar_system_source_id: conn.solar_system_source,
|
||||
solar_system_target_id: conn.solar_system_target,
|
||||
locked: value
|
||||
})
|
||||
end
|
||||
|
||||
@doc "Creates a connection between two systems"
|
||||
@spec create_connection(String.t(), map(), String.t()) ::
|
||||
{:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
|
||||
@@ -5,9 +5,42 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
|
||||
# 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 internal character UUID)
|
||||
{:ok, fallback_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}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Invalid format
|
||||
{:error, :invalid_character}
|
||||
end
|
||||
end
|
||||
|
||||
# Handle nil or non-map params by falling back to owner's character
|
||||
defp validate_character_eve_id(_params, fallback_char_id) do
|
||||
{:ok, fallback_char_id}
|
||||
end
|
||||
|
||||
@spec list_signatures(String.t()) :: [map()]
|
||||
def list_signatures(map_id) do
|
||||
systems = Operations.list_systems(map_id)
|
||||
@@ -41,11 +74,14 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
# Convert solar_system_id to system_id for internal use
|
||||
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
# Validate character first, then convert solar_system_id to system_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", char_id)
|
||||
|> Map.put("system_id", system.id)
|
||||
|> Map.delete("solar_system_id")
|
||||
|
||||
@@ -54,7 +90,7 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: char_id,
|
||||
character_id: validated_char_uuid, # Pass internal UUID here
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
@@ -86,6 +122,10 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_character} ->
|
||||
Logger.error("[create_signature] Invalid character_eve_id provided")
|
||||
{:error, :invalid_character}
|
||||
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
@@ -111,7 +151,10 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
sig_id,
|
||||
params
|
||||
) do
|
||||
with {:ok, sig} <- MapSystemSignature.by_id(sig_id),
|
||||
# Validate character first, then look up signature and system
|
||||
# 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 = %{
|
||||
"eve_id" => sig.eve_id,
|
||||
@@ -120,11 +163,11 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
"group" => sig.group,
|
||||
"type" => sig.type,
|
||||
"custom_info" => sig.custom_info,
|
||||
"character_eve_id" => 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 =
|
||||
@@ -133,7 +176,7 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
updated_signatures: [attrs],
|
||||
removed_signatures: [],
|
||||
solar_system_id: system.solar_system_id,
|
||||
character_id: char_id,
|
||||
character_id: validated_char_uuid, # Pass internal UUID here
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
@@ -151,6 +194,10 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
_ -> {:ok, attrs}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_character} ->
|
||||
Logger.error("[update_signature] Invalid character_eve_id provided")
|
||||
{:error, :invalid_character}
|
||||
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -373,36 +373,36 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_target: solar_system_target
|
||||
} = updated_connection
|
||||
) do
|
||||
source_system =
|
||||
WandererApp.Map.find_system_by_location(
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_source}
|
||||
),
|
||||
target_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_target}
|
||||
),
|
||||
source_linked_signatures <-
|
||||
find_linked_signatures(source_system, target_system),
|
||||
target_linked_signatures <- find_linked_signatures(target_system, source_system) do
|
||||
update_signatures_time_status(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_source}
|
||||
source_system.solar_system_id,
|
||||
source_linked_signatures,
|
||||
time_status
|
||||
)
|
||||
|
||||
target_system =
|
||||
WandererApp.Map.find_system_by_location(
|
||||
update_signatures_time_status(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_target}
|
||||
target_system.solar_system_id,
|
||||
target_linked_signatures,
|
||||
time_status
|
||||
)
|
||||
|
||||
source_linked_signatures =
|
||||
find_linked_signatures(source_system, target_system)
|
||||
|
||||
target_linked_signatures = find_linked_signatures(target_system, source_system)
|
||||
|
||||
update_signatures_time_status(
|
||||
map_id,
|
||||
source_system.solar_system_id,
|
||||
source_linked_signatures,
|
||||
time_status
|
||||
)
|
||||
|
||||
update_signatures_time_status(
|
||||
map_id,
|
||||
target_system.solar_system_id,
|
||||
target_linked_signatures,
|
||||
time_status
|
||||
)
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to update_linked_signature_time_status: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp find_linked_signatures(
|
||||
@@ -438,7 +438,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
%{custom_info: updated_custom_info}
|
||||
end
|
||||
|
||||
SignaturesImpl.apply_update_signature(%{map_id: map_id}, sig, update_params)
|
||||
SignaturesImpl.apply_update_signature(map_id, sig, update_params)
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -38,14 +38,18 @@
|
||||
</div>
|
||||
<div class="navbar-end"></div>
|
||||
</navbar>
|
||||
<div class="!z-10 min-h-[calc(100vh-7rem)]">
|
||||
<div class="!z-10 min-h-[calc(100vh-11rem)]">
|
||||
{@inner_content}
|
||||
</div>
|
||||
<!--Footer-->
|
||||
<footer class="!z-10 w-full pb-4 text-sm text-center fade-in">
|
||||
<a class="text-gray-500 no-underline hover:no-underline" href="#">
|
||||
© Wanderer 2024
|
||||
</a>
|
||||
<footer class="!z-10 w-full pt-8 pb-4 text-sm text-center fade-in flex justify-center items-center">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.eveonline.com/partners"><img src="/images/eo_pp.png" style="width: 300px;" alt="Eve Online Partnership Program"></a>
|
||||
<div class="text-gray-500 no-underline hover:no-underline">
|
||||
All <a href="/license">EVE related materials</a> are property of <a href="https://www.ccpgames.com">CCP Games</a>
|
||||
© {Date.utc_today().year} Wanderer Industries.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="fixed top-0 left-0 w-full h-full !-z-1 maps_bg" />
|
||||
</main>
|
||||
|
||||
@@ -432,32 +432,42 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
description: "Solar System ID (EVE Online system ID, e.g., 30000142 for Jita)",
|
||||
type: :integer,
|
||||
required: true,
|
||||
example: 30_000_142
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@detail_response_schema)
|
||||
)
|
||||
|
||||
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
|
||||
with {:ok, system_uuid} <- APIUtils.validate_uuid(id),
|
||||
{:ok, system} <- WandererApp.Api.MapSystem.by_id(system_uuid) do
|
||||
# Verify the system belongs to the requested map
|
||||
if system.map_id == map_id do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
else
|
||||
# Look up by solar_system_id (EVE Online integer ID)
|
||||
case APIUtils.parse_int(id) do
|
||||
{:ok, solar_system_id} ->
|
||||
case Operations.get_system(map_id, solar_system_id) do
|
||||
{:ok, system} ->
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(system))
|
||||
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
else
|
||||
{:error, %Ash.Error.Query.NotFound{}} -> {:error, :not_found}
|
||||
{:error, _} -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
operation(:create,
|
||||
summary: "Upsert Systems and Connections (batch or single)",
|
||||
summary: "Create or Update Systems and Connections",
|
||||
description: """
|
||||
Creates or updates systems and connections. Supports two formats:
|
||||
|
||||
1. **Single System Format**: Post a single system object directly (e.g., `{"solar_system_id": 30000142, "position_x": 100, ...}`)
|
||||
2. **Batch Format**: Post multiple systems and connections (e.g., `{"systems": [...], "connections": [...]}`)
|
||||
|
||||
Systems are identified by solar_system_id and will be updated if they already exist on the map.
|
||||
""",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
@@ -472,8 +482,22 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
)
|
||||
|
||||
def create(conn, params) do
|
||||
systems = Map.get(params, "systems", [])
|
||||
connections = Map.get(params, "connections", [])
|
||||
# Support both batch format {"systems": [...], "connections": [...]}
|
||||
# and single system format {"solar_system_id": ..., ...}
|
||||
{systems, connections} =
|
||||
cond do
|
||||
Map.has_key?(params, "systems") ->
|
||||
# Batch format
|
||||
{Map.get(params, "systems", []), Map.get(params, "connections", [])}
|
||||
|
||||
Map.has_key?(params, "solar_system_id") or Map.has_key?(params, :solar_system_id) ->
|
||||
# Single system format - wrap it in an array
|
||||
{[params], []}
|
||||
|
||||
true ->
|
||||
# Empty request
|
||||
{[], []}
|
||||
end
|
||||
|
||||
case Operations.upsert_systems_and_connections(conn, systems, connections) do
|
||||
{:ok, result} ->
|
||||
@@ -496,9 +520,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
description: "Solar System ID (EVE Online system ID, e.g., 30000142 for Jita)",
|
||||
type: :integer,
|
||||
required: true,
|
||||
example: 30_000_142
|
||||
]
|
||||
],
|
||||
request_body: {"System update request", "application/json", @system_update_schema},
|
||||
@@ -506,11 +531,15 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
)
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
with {:ok, system_uuid} <- APIUtils.validate_uuid(id),
|
||||
{:ok, system} <- WandererApp.Api.MapSystem.by_id(system_uuid),
|
||||
{:ok, attrs} <- APIUtils.extract_update_params(params),
|
||||
{:ok, updated_system} <- Ash.update(system, attrs) do
|
||||
APIUtils.respond_data(conn, APIUtils.map_system_to_json(updated_system))
|
||||
with {:ok, solar_system_id} <- APIUtils.parse_int(id),
|
||||
{:ok, attrs} <- APIUtils.extract_update_params(params) do
|
||||
case Operations.update_system(conn, solar_system_id, attrs) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -578,9 +607,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
],
|
||||
id: [
|
||||
in: :path,
|
||||
description: "System ID",
|
||||
type: :string,
|
||||
required: true
|
||||
description: "Solar System ID (EVE Online system ID, e.g., 30000142 for Jita)",
|
||||
type: :integer,
|
||||
required: true,
|
||||
example: 30_000_142
|
||||
]
|
||||
],
|
||||
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||
|
||||
@@ -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"}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -353,7 +353,7 @@ defmodule WandererAppWeb.Helpers.APIUtils do
|
||||
def connection_to_json(conn) do
|
||||
Map.take(conn, ~w(
|
||||
id map_id solar_system_source solar_system_target mass_status
|
||||
time_status ship_size_type type wormhole_type inserted_at updated_at
|
||||
time_status ship_size_type type wormhole_type locked inserted_at updated_at
|
||||
)a)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -272,6 +272,9 @@
|
||||
<.icon name="hero-check-badge-solid" class="w-5 h-5" />
|
||||
</div>
|
||||
</:col>
|
||||
<:col :let={subscription} label="Map">
|
||||
{subscription.map.name}
|
||||
</:col>
|
||||
<:col :let={subscription} label="Active Till">
|
||||
<.local_time
|
||||
:if={subscription.active_till}
|
||||
@@ -333,7 +336,7 @@
|
||||
label="Valid"
|
||||
options={Enum.map(@valid_types, fn valid_type -> {valid_type.label, valid_type.id} end)}
|
||||
/>
|
||||
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="modal-action">
|
||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -597,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
|
||||
@@ -605,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
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.84.1"
|
||||
@version "1.84.13"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapPerformanceIndexes do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create index(:map_system_v1, [:map_id],
|
||||
name: "map_system_v1_map_id_visible_index",
|
||||
where: "visible = true"
|
||||
)
|
||||
|
||||
create index(:map_chain_v1, [:map_id], name: "map_chain_v1_map_id_index")
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists index(:map_chain_v1, [:map_id], name: "map_chain_v1_map_id_index")
|
||||
|
||||
drop_if_exists index(:map_system_v1, [:map_id], name: "map_system_v1_map_id_visible_index")
|
||||
end
|
||||
end
|
||||
144
priv/repo/migrations/20251112000000_fix_duplicate_map_slugs.exs
Normal file
144
priv/repo/migrations/20251112000000_fix_duplicate_map_slugs.exs
Normal file
@@ -0,0 +1,144 @@
|
||||
defmodule WandererApp.Repo.Migrations.FixDuplicateMapSlugs do
|
||||
use Ecto.Migration
|
||||
import Ecto.Query
|
||||
|
||||
def up do
|
||||
# Check for duplicates first
|
||||
has_duplicates = check_for_duplicates()
|
||||
|
||||
# If duplicates exist, drop the index first to allow fixing them
|
||||
if has_duplicates do
|
||||
IO.puts("Duplicates found, dropping index before cleanup...")
|
||||
drop_index_if_exists()
|
||||
end
|
||||
|
||||
# Fix duplicate slugs in maps_v1 table
|
||||
fix_duplicate_slugs()
|
||||
|
||||
# Ensure unique index exists (recreate if needed)
|
||||
ensure_unique_index()
|
||||
end
|
||||
|
||||
def down do
|
||||
# This migration is idempotent and safe to run multiple times
|
||||
# No need to revert as it only fixes data integrity issues
|
||||
:ok
|
||||
end
|
||||
|
||||
defp check_for_duplicates do
|
||||
duplicates_query = """
|
||||
SELECT COUNT(*) as duplicate_count
|
||||
FROM (
|
||||
SELECT slug
|
||||
FROM maps_v1
|
||||
GROUP BY slug
|
||||
HAVING count(*) > 1
|
||||
) duplicates
|
||||
"""
|
||||
|
||||
case repo().query(duplicates_query, []) do
|
||||
{:ok, %{rows: [[count]]}} when count > 0 ->
|
||||
IO.puts("Found #{count} duplicate slug(s)")
|
||||
true
|
||||
|
||||
{:ok, %{rows: [[0]]}} ->
|
||||
false
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("Error checking for duplicates: #{inspect(error)}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_index_if_exists do
|
||||
index_exists_query = """
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'maps_v1'
|
||||
AND indexname = 'maps_v1_unique_slug_index'
|
||||
)
|
||||
"""
|
||||
|
||||
case repo().query(index_exists_query, []) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
IO.puts("Dropping existing unique index...")
|
||||
execute("DROP INDEX IF EXISTS maps_v1_unique_slug_index")
|
||||
IO.puts("✓ Index dropped")
|
||||
|
||||
{:ok, %{rows: [[false]]}} ->
|
||||
IO.puts("No existing index to drop")
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("Error checking index: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_duplicate_slugs do
|
||||
# Get all duplicate slugs with their IDs
|
||||
duplicates_query = """
|
||||
SELECT slug, array_agg(id::text ORDER BY updated_at) as ids
|
||||
FROM maps_v1
|
||||
GROUP BY slug
|
||||
HAVING count(*) > 1
|
||||
"""
|
||||
|
||||
case repo().query(duplicates_query, []) do
|
||||
{:ok, %{rows: rows}} when length(rows) > 0 ->
|
||||
IO.puts("Fixing #{length(rows)} duplicate slug(s)...")
|
||||
|
||||
Enum.each(rows, fn [slug, ids] ->
|
||||
IO.puts("Processing duplicate slug: #{slug} (#{length(ids)} occurrences)")
|
||||
|
||||
# Keep the first one (oldest), rename the rest
|
||||
[_keep_id | rename_ids] = ids
|
||||
|
||||
rename_ids
|
||||
|> Enum.with_index(2)
|
||||
|> Enum.each(fn {id_string, n} ->
|
||||
new_slug = "#{slug}-#{n}"
|
||||
|
||||
# Use parameterized query for safety
|
||||
update_query = "UPDATE maps_v1 SET slug = $1 WHERE id::text = $2"
|
||||
repo().query!(update_query, [new_slug, id_string])
|
||||
IO.puts(" ✓ Renamed #{id_string} to '#{new_slug}'")
|
||||
end)
|
||||
end)
|
||||
|
||||
IO.puts("✓ All duplicate slugs fixed!")
|
||||
|
||||
{:ok, %{rows: []}} ->
|
||||
IO.puts("No duplicate slugs to fix")
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("Error checking for duplicates: #{inspect(error)}")
|
||||
raise "Failed to check for duplicate slugs: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_unique_index do
|
||||
# Check if index exists
|
||||
index_exists_query = """
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'maps_v1'
|
||||
AND indexname = 'maps_v1_unique_slug_index'
|
||||
)
|
||||
"""
|
||||
|
||||
case repo().query(index_exists_query, []) do
|
||||
{:ok, %{rows: [[true]]}} ->
|
||||
IO.puts("Unique index on slug already exists")
|
||||
|
||||
{:ok, %{rows: [[false]]}} ->
|
||||
IO.puts("Creating unique index on slug...")
|
||||
create_if_not_exists index(:maps_v1, [:slug], unique: true, name: :maps_v1_unique_slug_index)
|
||||
IO.puts("✓ Index created successfully!")
|
||||
|
||||
{:error, error} ->
|
||||
IO.puts("Error checking index: #{inspect(error)}")
|
||||
raise "Failed to check index: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
201
priv/resource_snapshots/repo/map_chain_v1/20251108142542.json
Normal file
201
priv/resource_snapshots/repo/map_chain_v1/20251108142542.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "solar_system_source",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "solar_system_target",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "mass_status",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "time_status",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "2",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "ship_size_type",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "type",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "wormhole_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "count_of_passage",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "locked",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "custom_info",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_chain_v1_map_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"map_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "map_id"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": "map_chain_v1_map_id_index",
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "43AE341D09AA875BB0F0D2ACE7AC6301064697D656FD1729FC36E6A1F77E4CB7",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_chain_v1"
|
||||
}
|
||||
260
priv/resource_snapshots/repo/map_system_v1/20251108142542.json
Normal file
260
priv/resource_snapshots/repo/map_system_v1/20251108142542.json
Normal file
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "solar_system_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "custom_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "tag",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "temporary_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "labels",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "status",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "true",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "visible",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "locked",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "position_x",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "position_y",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "added_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "linked_sig_eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_system_v1_map_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"concurrently": false,
|
||||
"error_fields": [
|
||||
"map_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "map_id"
|
||||
}
|
||||
],
|
||||
"include": null,
|
||||
"message": null,
|
||||
"name": "map_system_v1_map_id_visible_index",
|
||||
"nulls_distinct": true,
|
||||
"prefix": null,
|
||||
"table": null,
|
||||
"unique": false,
|
||||
"using": null,
|
||||
"where": "visible = true"
|
||||
}
|
||||
],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "AD7B82611EDA495AD35F114406C7F0C2D941C10E51105361002AA3144D7F7EA9",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "map_system_v1_map_solar_system_id_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "map_id"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "solar_system_id"
|
||||
}
|
||||
],
|
||||
"name": "map_solar_system_id",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_system_v1"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
18
test/manual/api/.env.example
Normal file
18
test/manual/api/.env.example
Normal 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
|
||||
249
test/manual/api/CURL_EXAMPLES.md
Normal file
249
test/manual/api/CURL_EXAMPLES.md
Normal 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
|
||||
289
test/manual/api/test_character_eve_id_fix.sh
Executable file
289
test/manual/api/test_character_eve_id_fix.sh
Executable 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 "═══════════════════════════════════════════════════════════════════"
|
||||
@@ -580,6 +580,155 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "character_eve_id validation" do
|
||||
test "create_signature uses provided character_eve_id when valid" do
|
||||
# Create a test character
|
||||
{:ok, character} =
|
||||
WandererApp.Api.Character.create(%{
|
||||
eve_id: "111111111",
|
||||
name: "Test Character"
|
||||
})
|
||||
|
||||
conn = %{
|
||||
assigns: %{
|
||||
map_id: Ecto.UUID.generate(),
|
||||
owner_character_id: "999999999",
|
||||
owner_user_id: Ecto.UUID.generate()
|
||||
}
|
||||
}
|
||||
|
||||
params = %{
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => character.eve_id
|
||||
}
|
||||
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
result = Signatures.create_signature(conn, params)
|
||||
|
||||
case result do
|
||||
{:ok, data} ->
|
||||
# Should use the provided character_eve_id, not the owner's
|
||||
assert Map.get(data, "character_eve_id") == character.eve_id
|
||||
|
||||
{:error, _} ->
|
||||
# System not found error is acceptable
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "create_signature rejects invalid character_eve_id" do
|
||||
conn = %{
|
||||
assigns: %{
|
||||
map_id: Ecto.UUID.generate(),
|
||||
owner_character_id: "999999999",
|
||||
owner_user_id: Ecto.UUID.generate()
|
||||
}
|
||||
}
|
||||
|
||||
params = %{
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC-123",
|
||||
"character_eve_id" => "invalid_char_id_999"
|
||||
}
|
||||
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
result = Signatures.create_signature(conn, params)
|
||||
# Should return invalid_character error
|
||||
assert {:error, :invalid_character} = result
|
||||
end)
|
||||
end
|
||||
|
||||
test "create_signature falls back to owner when character_eve_id not provided" do
|
||||
owner_char_id = "888888888"
|
||||
|
||||
conn = %{
|
||||
assigns: %{
|
||||
map_id: Ecto.UUID.generate(),
|
||||
owner_character_id: owner_char_id,
|
||||
owner_user_id: Ecto.UUID.generate()
|
||||
}
|
||||
}
|
||||
|
||||
params = %{
|
||||
"solar_system_id" => 30_000_142,
|
||||
"eve_id" => "ABC-123"
|
||||
}
|
||||
|
||||
MapTestHelpers.expect_map_server_error(fn ->
|
||||
result = Signatures.create_signature(conn, params)
|
||||
|
||||
case result do
|
||||
{:ok, data} ->
|
||||
# Should use the owner's character_eve_id
|
||||
assert Map.get(data, "character_eve_id") == owner_char_id
|
||||
|
||||
{:error, _} ->
|
||||
# System not found error is acceptable
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "update_signature respects provided character_eve_id when valid" do
|
||||
# Create a test character
|
||||
{:ok, character} =
|
||||
WandererApp.Api.Character.create(%{
|
||||
eve_id: "222222222",
|
||||
name: "Another Test Character"
|
||||
})
|
||||
|
||||
conn = %{
|
||||
assigns: %{
|
||||
map_id: Ecto.UUID.generate(),
|
||||
owner_character_id: "999999999",
|
||||
owner_user_id: Ecto.UUID.generate()
|
||||
}
|
||||
}
|
||||
|
||||
sig_id = Ecto.UUID.generate()
|
||||
|
||||
params = %{
|
||||
"name" => "Updated Name",
|
||||
"character_eve_id" => character.eve_id
|
||||
}
|
||||
|
||||
result = Signatures.update_signature(conn, sig_id, params)
|
||||
|
||||
case result do
|
||||
{:ok, data} ->
|
||||
# Should use the provided character_eve_id
|
||||
assert Map.get(data, "character_eve_id") == character.eve_id
|
||||
|
||||
{:error, _} ->
|
||||
# Signature not found error is acceptable
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
test "update_signature rejects invalid character_eve_id" do
|
||||
conn = %{
|
||||
assigns: %{
|
||||
map_id: Ecto.UUID.generate(),
|
||||
owner_character_id: "999999999",
|
||||
owner_user_id: Ecto.UUID.generate()
|
||||
}
|
||||
}
|
||||
|
||||
sig_id = Ecto.UUID.generate()
|
||||
|
||||
params = %{
|
||||
"name" => "Updated Name",
|
||||
"character_eve_id" => "totally_invalid_char"
|
||||
}
|
||||
|
||||
result = Signatures.update_signature(conn, sig_id, params)
|
||||
# Should return invalid_character error
|
||||
assert {:error, :invalid_character} = result
|
||||
end
|
||||
end
|
||||
|
||||
describe "parameter merging and character_eve_id injection" do
|
||||
test "create_signature injects character_eve_id correctly" do
|
||||
char_id = "987654321"
|
||||
|
||||
Reference in New Issue
Block a user