mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-03 14:32:36 +00:00
Compare commits
1 Commits
v1.89.6
...
revert-561
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
646262447d |
2
Makefile
2
Makefile
@@ -33,7 +33,7 @@ test t:
|
||||
MIX_ENV=test mix test
|
||||
|
||||
coverage cover co:
|
||||
MIX_ENV=test mix test --cover
|
||||
mix test --cover
|
||||
|
||||
unit-tests ut:
|
||||
@echo "Running unit tests..."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CommentType } from '@/hooks/Mapper/types';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export interface CommentsProps {}
|
||||
|
||||
@@ -14,9 +14,7 @@ export const Comments = ({}: CommentsProps) => {
|
||||
comments: { loadComments, comments, lastUpdateKey },
|
||||
} = useMapRootState();
|
||||
|
||||
const systemId = useMemo(() => {
|
||||
return +selectedSystems[0];
|
||||
}, [selectedSystems]);
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const ref = useRef({ loadComments, systemId });
|
||||
ref.current = { loadComments, systemId };
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import classes from './CommentsEditor.module.scss';
|
||||
@@ -19,9 +19,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const systemId = useMemo(() => {
|
||||
return +selectedSystems[0];
|
||||
}, [selectedSystems]);
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const ref = useRef({ outCommand, systemId, textVal });
|
||||
ref.current = { outCommand, systemId, textVal };
|
||||
|
||||
@@ -12,7 +12,7 @@ export const useCommandComments = () => {
|
||||
}, []);
|
||||
|
||||
const removeComment = useCallback((data: CommandCommentRemoved) => {
|
||||
ref.current.removeComment(data.solarSystemId, data.commentId);
|
||||
ref.current.removeComment(data.solarSystemId.toString(), data.commentId);
|
||||
}, []);
|
||||
|
||||
return { addComment, removeComment };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
|
||||
|
||||
interface UseCommentsProps {
|
||||
outCommand: OutCommandHandler;
|
||||
@@ -8,12 +8,12 @@ interface UseCommentsProps {
|
||||
export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => {
|
||||
const [lastUpdateKey, setLastUpdateKey] = useState(0);
|
||||
|
||||
const commentBySystemsRef = useRef<Map<number, CommentSystem>>(new Map());
|
||||
const commentBySystemsRef = useRef<Map<string, CommentSystem>>(new Map());
|
||||
|
||||
const ref = useRef({ outCommand });
|
||||
ref.current = { outCommand };
|
||||
|
||||
const loadComments = useCallback(async (systemId: number) => {
|
||||
const loadComments = useCallback(async (systemId: string) => {
|
||||
let cSystem = commentBySystemsRef.current.get(systemId);
|
||||
if (cSystem?.loading || cSystem?.loaded) {
|
||||
return;
|
||||
@@ -45,7 +45,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
||||
setLastUpdateKey(x => x + 1);
|
||||
}, []);
|
||||
|
||||
const addComment = useCallback((systemId: number, comment: CommentType) => {
|
||||
const addComment = useCallback((systemId: string, comment: CommentType) => {
|
||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||
if (cSystem) {
|
||||
cSystem.comments.push(comment);
|
||||
@@ -61,9 +61,8 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
||||
setLastUpdateKey(x => x + 1);
|
||||
}, []);
|
||||
|
||||
const removeComment = useCallback((systemId: number, commentId: string) => {
|
||||
const removeComment = useCallback((systemId: string, commentId: string) => {
|
||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||
console.log('cSystem', cSystem);
|
||||
if (!cSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ export type CommentSystem = {
|
||||
};
|
||||
|
||||
export interface UseCommentsData {
|
||||
loadComments: (systemId: number) => Promise<void>;
|
||||
addComment: (systemId: number, comment: CommentType) => void;
|
||||
removeComment: (systemId: number, commentId: string) => void;
|
||||
comments: Map<number, CommentSystem>;
|
||||
loadComments: (systemId: string) => Promise<void>;
|
||||
addComment: (systemId: string, comment: CommentType) => void;
|
||||
removeComment: (systemId: string, commentId: string) => void;
|
||||
comments: Map<string, CommentSystem>;
|
||||
lastUpdateKey: number;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export type CommandLinkSignatureToSystem = {
|
||||
};
|
||||
export type CommandLinkSignaturesUpdated = number;
|
||||
export type CommandCommentAdd = {
|
||||
solarSystemId: number;
|
||||
solarSystemId: string;
|
||||
comment: CommentType;
|
||||
};
|
||||
export type CommandCommentRemoved = {
|
||||
|
||||
@@ -432,7 +432,7 @@ config :wanderer_app, :license_manager,
|
||||
config :wanderer_app, :sse,
|
||||
enabled:
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "false")
|
||||
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true")
|
||||
|> String.to_existing_atom(),
|
||||
max_connections_total:
|
||||
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
|
||||
@@ -447,6 +447,6 @@ config :wanderer_app, :sse,
|
||||
config :wanderer_app, :external_events,
|
||||
webhooks_enabled:
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "false")
|
||||
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true")
|
||||
|> String.to_existing_atom(),
|
||||
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)
|
||||
|
||||
@@ -24,11 +24,7 @@ config :wanderer_app,
|
||||
pubsub_client: Test.PubSubMock,
|
||||
cached_info: WandererApp.CachedInfo.Mock,
|
||||
character_api_disabled: false,
|
||||
environment: :test,
|
||||
map_subscriptions_enabled: false,
|
||||
wanderer_kills_service_enabled: false,
|
||||
sse: [enabled: false],
|
||||
external_events: [webhooks_enabled: false]
|
||||
environment: :test
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
|
||||
@@ -60,17 +60,19 @@ defmodule WandererApp.Api.AccessList do
|
||||
# Added :api_key to the accepted attributes
|
||||
accept [:name, :description, :owner_id, :api_key]
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
update :update do
|
||||
accept [:name, :description, :owner_id, :api_key]
|
||||
primary?(true)
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :assign_owner do
|
||||
accept [:owner_id]
|
||||
require_atomic? false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -53,11 +53,7 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
:role
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :read_by_access_list do
|
||||
argument(:access_list_id, :string, allow_nil?: false)
|
||||
@@ -71,14 +67,12 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
update :block do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:blocked, true))
|
||||
end
|
||||
|
||||
update :unblock do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:blocked, false))
|
||||
end
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
defmodule WandererApp.Api.ActorHelpers do
|
||||
@moduledoc """
|
||||
Utilities for extracting actor information from Ash contexts.
|
||||
|
||||
Provides helper functions for working with ActorWithMap and extracting
|
||||
user, map, and character information from various context formats.
|
||||
"""
|
||||
|
||||
alias WandererApp.Api.ActorWithMap
|
||||
|
||||
@doc """
|
||||
Extract map from actor or context.
|
||||
|
||||
Handles various context formats:
|
||||
- Direct ActorWithMap struct
|
||||
- Context map with :actor key
|
||||
- Context map with :map key
|
||||
- Ash.Resource.Change.Context struct
|
||||
"""
|
||||
def get_map(%{actor: %ActorWithMap{map: %{} = map}}), do: map
|
||||
def get_map(%{map: %{} = map}), do: map
|
||||
|
||||
# Handle Ash.Resource.Change.Context struct
|
||||
def get_map(%Ash.Resource.Change.Context{actor: %ActorWithMap{map: %{} = map}}), do: map
|
||||
def get_map(%Ash.Resource.Change.Context{actor: _}), do: nil
|
||||
|
||||
def get_map(context) when is_map(context) do
|
||||
# For plain maps, check private.actor
|
||||
with private when is_map(private) <- Map.get(context, :private),
|
||||
%ActorWithMap{map: %{} = map} <- Map.get(private, :actor) do
|
||||
map
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_map(_), do: nil
|
||||
|
||||
@doc """
|
||||
Extract user from actor.
|
||||
|
||||
Handles:
|
||||
- ActorWithMap struct
|
||||
- Direct user struct with :id field
|
||||
"""
|
||||
def get_user(%ActorWithMap{user: user}), do: user
|
||||
def get_user(%{id: _} = user), do: user
|
||||
def get_user(_), do: nil
|
||||
|
||||
@doc """
|
||||
Get character IDs for the actor.
|
||||
|
||||
Used for ACL filtering to determine which resources the user can access.
|
||||
Returns {:ok, list} or {:ok, []} if no characters found.
|
||||
"""
|
||||
def get_character_ids(%ActorWithMap{user: user}), do: get_character_ids(user)
|
||||
|
||||
def get_character_ids(%{characters: characters}) when is_list(characters) do
|
||||
{:ok, Enum.map(characters, & &1.id)}
|
||||
end
|
||||
|
||||
def get_character_ids(%{characters: %Ecto.Association.NotLoaded{}, id: user_id}) do
|
||||
# Load characters from database
|
||||
load_characters_by_id(user_id)
|
||||
end
|
||||
|
||||
def get_character_ids(%{id: user_id}) do
|
||||
# Fallback: load user with characters
|
||||
load_characters_by_id(user_id)
|
||||
end
|
||||
|
||||
def get_character_ids(_), do: {:ok, []}
|
||||
|
||||
defp load_characters_by_id(user_id) do
|
||||
case WandererApp.Api.User.by_id(user_id, load: [:characters]) do
|
||||
{:ok, user} -> {:ok, Enum.map(user.characters, & &1.id)}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
defmodule WandererApp.Api.ActorWithMap do
|
||||
@moduledoc """
|
||||
Wraps a user and map together as an actor for token-based authentication.
|
||||
|
||||
When API requests use Bearer token auth, the token identifies both the user
|
||||
(map owner) and the map. This struct allows passing both through Ash's actor system.
|
||||
"""
|
||||
|
||||
@enforce_keys [:user, :map]
|
||||
defstruct [:user, :map]
|
||||
|
||||
def new(user, map) do
|
||||
%__MODULE__{user: user, map: map}
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
||||
@moduledoc """
|
||||
Ash change that injects map_id from the authenticated actor.
|
||||
|
||||
For token-based auth, the map is determined by the API token.
|
||||
This change automatically sets map_id, so clients don't need to provide it.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
alias WandererApp.Api.ActorHelpers
|
||||
|
||||
@impl true
|
||||
def change(changeset, _opts, context) do
|
||||
case ActorHelpers.get_map(context) do
|
||||
%{id: map_id} ->
|
||||
Ash.Changeset.force_change_attribute(changeset, :map_id, map_id)
|
||||
|
||||
_other ->
|
||||
# nil or unexpected return shape - check for direct map_id
|
||||
# Check params (input), arguments, and attributes (in that order)
|
||||
map_id = Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
|
||||
case map_id do
|
||||
nil ->
|
||||
Ash.Changeset.add_error(changeset,
|
||||
field: :map_id,
|
||||
message: "map_id is required (provide via token or attribute)"
|
||||
)
|
||||
|
||||
_map_id ->
|
||||
# map_id provided directly (internal calls, tests)
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -69,6 +69,11 @@ defmodule WandererApp.Api.Character do
|
||||
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||
end
|
||||
|
||||
read :available_by_map do
|
||||
argument(:map_id, :uuid, allow_nil?: false)
|
||||
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||
end
|
||||
|
||||
read :last_active do
|
||||
argument(:from, :utc_datetime, allow_nil?: false)
|
||||
|
||||
@@ -95,7 +100,6 @@ defmodule WandererApp.Api.Character do
|
||||
|
||||
update :mark_as_deleted do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(atomic_update(:deleted, true))
|
||||
change(atomic_update(:user_id, nil))
|
||||
@@ -103,7 +107,6 @@ defmodule WandererApp.Api.Character do
|
||||
|
||||
update :update_online do
|
||||
accept([:online])
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_location do
|
||||
|
||||
@@ -33,11 +33,7 @@ defmodule WandererApp.Api.CorpWalletTransaction do
|
||||
:ref_type
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
|
||||
@@ -36,11 +36,7 @@ defmodule WandererApp.Api.License do
|
||||
:expire_at
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
@@ -62,14 +58,12 @@ defmodule WandererApp.Api.License do
|
||||
|
||||
update :invalidate do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:is_valid, false))
|
||||
end
|
||||
|
||||
update :set_valid do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:is_valid, true))
|
||||
end
|
||||
|
||||
@@ -8,8 +8,6 @@ defmodule WandererApp.Api.Map do
|
||||
|
||||
alias Ash.Resource.Change.Builtins
|
||||
|
||||
require Logger
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("maps_v1")
|
||||
@@ -46,7 +44,6 @@ defmodule WandererApp.Api.Map do
|
||||
code_interface do
|
||||
define(:available, action: :available)
|
||||
define(:get_map_by_slug, action: :by_slug, args: [:slug])
|
||||
define(:by_api_key, action: :by_api_key, args: [:api_key])
|
||||
define(:new, action: :new)
|
||||
define(:create, action: :create)
|
||||
define(:update, action: :update)
|
||||
@@ -57,7 +54,6 @@ defmodule WandererApp.Api.Map do
|
||||
define(:mark_as_deleted, action: :mark_as_deleted)
|
||||
define(:update_api_key, action: :update_api_key)
|
||||
define(:toggle_webhooks, action: :toggle_webhooks)
|
||||
define(:toggle_sse, action: :toggle_sse)
|
||||
|
||||
define(:by_id,
|
||||
get_by: [:id],
|
||||
@@ -94,34 +90,22 @@ defmodule WandererApp.Api.Map do
|
||||
filter expr(slug == ^arg(:slug))
|
||||
end
|
||||
|
||||
read :by_api_key do
|
||||
get? true
|
||||
argument :api_key, :string, allow_nil?: false
|
||||
|
||||
prepare WandererApp.Api.Preparations.SecureApiKeyLookup
|
||||
end
|
||||
|
||||
read :available do
|
||||
prepare WandererApp.Api.Preparations.FilterMapsByRoles
|
||||
end
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
:name,
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
]
|
||||
|
||||
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id, :uuid, allow_nil?: false
|
||||
argument :create_default_acl, :boolean, allow_nil?: true
|
||||
argument :acls, {:array, :uuid}, allow_nil?: true
|
||||
argument :acls_text_input, :string, allow_nil?: true
|
||||
argument :scope_text_input, :string, allow_nil?: true
|
||||
argument :acls_empty_selection, :string, allow_nil?: true
|
||||
|
||||
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:acls, type: :append_and_remove)
|
||||
change WandererApp.Api.Changes.SlugifyName
|
||||
end
|
||||
@@ -129,16 +113,7 @@ defmodule WandererApp.Api.Map do
|
||||
update :update do
|
||||
primary? true
|
||||
require_atomic? false
|
||||
|
||||
accept [
|
||||
:name,
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
]
|
||||
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
|
||||
|
||||
argument :owner_id_text_input, :string, allow_nil?: true
|
||||
argument :acls_text_input, :string, allow_nil?: true
|
||||
@@ -153,9 +128,6 @@ defmodule WandererApp.Api.Map do
|
||||
)
|
||||
|
||||
change WandererApp.Api.Changes.SlugifyName
|
||||
|
||||
# Validate subscription when enabling SSE
|
||||
validate &validate_sse_subscription/2
|
||||
end
|
||||
|
||||
update :update_acls do
|
||||
@@ -170,46 +142,33 @@ defmodule WandererApp.Api.Map do
|
||||
|
||||
update :assign_owner do
|
||||
accept [:owner_id]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_hubs do
|
||||
accept [:hubs]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_options do
|
||||
accept [:options]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :mark_as_deleted do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:deleted, true))
|
||||
end
|
||||
|
||||
update :update_api_key do
|
||||
accept [:public_api_key]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :toggle_webhooks do
|
||||
accept [:webhooks_enabled]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :toggle_sse do
|
||||
require_atomic? false
|
||||
accept [:sse_enabled]
|
||||
|
||||
# Validate subscription when enabling SSE
|
||||
validate &validate_sse_subscription/2
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
accept [:name, :description, :scope, :only_tracked_characters]
|
||||
|
||||
argument :source_map_id, :uuid, allow_nil?: false
|
||||
argument :copy_acls, :boolean, default: true
|
||||
argument :copy_user_settings, :boolean, default: true
|
||||
@@ -353,19 +312,12 @@ defmodule WandererApp.Api.Map do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :sse_enabled, :boolean do
|
||||
default(false)
|
||||
allow_nil?(false)
|
||||
public?(true)
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_slug, [:slug]
|
||||
identity :unique_public_api_key, [:public_api_key]
|
||||
end
|
||||
|
||||
relationships do
|
||||
@@ -392,49 +344,4 @@ defmodule WandererApp.Api.Map do
|
||||
public? false
|
||||
end
|
||||
end
|
||||
|
||||
# SSE Subscription Validation
|
||||
#
|
||||
# This validation ensures that SSE can only be enabled when:
|
||||
# 1. SSE is being disabled (always allowed)
|
||||
# 2. Map is being created (skip validation, will be checked on first update)
|
||||
# 3. Community Edition mode (always allowed)
|
||||
# 4. Enterprise mode with active subscription
|
||||
defp validate_sse_subscription(changeset, _context) do
|
||||
sse_enabled = Ash.Changeset.get_attribute(changeset, :sse_enabled)
|
||||
map_id = changeset.data.id
|
||||
subscriptions_enabled = WandererApp.Env.map_subscriptions_enabled?()
|
||||
|
||||
cond do
|
||||
# Not enabling SSE - no validation needed
|
||||
not sse_enabled ->
|
||||
:ok
|
||||
|
||||
# Map creation (no ID yet) - skip validation
|
||||
is_nil(map_id) ->
|
||||
:ok
|
||||
|
||||
# Community Edition mode - always allow
|
||||
not subscriptions_enabled ->
|
||||
:ok
|
||||
|
||||
# Enterprise mode - check subscription
|
||||
true ->
|
||||
validate_active_subscription(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_active_subscription(map_id) do
|
||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
||||
{:ok, true} ->
|
||||
:ok
|
||||
|
||||
{:ok, false} ->
|
||||
{:error, field: :sse_enabled, message: "Active subscription required to enable SSE"}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Error checking subscription status: #{inspect(reason)}")
|
||||
{:error, field: :sse_enabled, message: "Unable to verify subscription status"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,11 +61,7 @@ defmodule WandererApp.Api.MapAccessList do
|
||||
:access_list_id
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :read_by_map do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
|
||||
@@ -27,11 +27,7 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
:solar_system_target_id
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
@@ -44,6 +40,12 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
]
|
||||
|
||||
primary?(true)
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
action :by_map_id, {:array, :struct} do
|
||||
|
||||
@@ -81,6 +81,12 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
:character_id,
|
||||
:tracked
|
||||
]
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
read :by_map_filtered do
|
||||
@@ -139,7 +145,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
update :track do
|
||||
accept [:map_id, :character_id]
|
||||
require_atomic? false
|
||||
argument :map_id, :string, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
# Load the record first
|
||||
load do
|
||||
@@ -152,7 +159,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
update :untrack do
|
||||
accept [:map_id, :character_id]
|
||||
require_atomic? false
|
||||
argument :map_id, :string, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
# Load the record first
|
||||
load do
|
||||
@@ -165,7 +173,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
update :follow do
|
||||
accept [:map_id, :character_id]
|
||||
require_atomic? false
|
||||
argument :map_id, :string, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
# Load the record first
|
||||
load do
|
||||
@@ -178,7 +187,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
update :unfollow do
|
||||
accept [:map_id, :character_id]
|
||||
require_atomic? false
|
||||
argument :map_id, :string, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
# Load the record first
|
||||
load do
|
||||
|
||||
@@ -4,8 +4,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshJsonApi.Resource],
|
||||
primary_read_warning?: false
|
||||
extensions: [AshJsonApi.Resource]
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
@@ -74,56 +73,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
:custom_info
|
||||
]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
accept [
|
||||
:map_id,
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:type,
|
||||
:ship_size_type,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
]
|
||||
|
||||
# Inject map_id from token
|
||||
change WandererApp.Api.Changes.InjectMapFromActor
|
||||
end
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
|
||||
# Security: Filter to only connections from actor's map
|
||||
prepare WandererApp.Api.Preparations.FilterConnectionsByActorMap
|
||||
end
|
||||
|
||||
update :update do
|
||||
primary? true
|
||||
|
||||
accept [
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:type,
|
||||
:ship_size_type,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
]
|
||||
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
destroy :destroy do
|
||||
primary? true
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :read_by_map do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
@@ -160,37 +110,30 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
update :update_mass_status do
|
||||
accept [:mass_status]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_time_status do
|
||||
accept [:time_status]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_ship_size_type do
|
||||
accept [:ship_size_type]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_locked do
|
||||
accept [:locked]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_custom_info do
|
||||
accept [:custom_info]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_type do
|
||||
accept [:type]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_wormhole_type do
|
||||
accept [:wormhole_type]
|
||||
require_atomic? false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -30,11 +30,7 @@ defmodule WandererApp.Api.MapInvite do
|
||||
:token
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
@@ -45,6 +41,10 @@ defmodule WandererApp.Api.MapInvite do
|
||||
]
|
||||
|
||||
primary?(true)
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: true
|
||||
|
||||
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
|
||||
@@ -3,8 +3,7 @@ defmodule WandererApp.Api.MapPing do
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
primary_read_warning?: false
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
@@ -37,18 +36,7 @@ defmodule WandererApp.Api.MapPing do
|
||||
:message
|
||||
]
|
||||
|
||||
defaults [:destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
|
||||
# Security: Filter to only pings from actor's map
|
||||
prepare WandererApp.Api.Preparations.FilterPingsByActorMap
|
||||
end
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :new do
|
||||
accept [
|
||||
@@ -60,6 +48,14 @@ defmodule WandererApp.Api.MapPing do
|
||||
]
|
||||
|
||||
primary?(true)
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
|
||||
@@ -65,11 +65,7 @@ defmodule WandererApp.Api.MapSolarSystem do
|
||||
:sun_type_id
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :destroy, :update]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
@@ -24,11 +24,7 @@ defmodule WandererApp.Api.MapSolarSystemJumps do
|
||||
:to_solar_system_id
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :destroy, :update]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
@@ -45,11 +45,7 @@ defmodule WandererApp.Api.MapState do
|
||||
:connections_start_time
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :update, :destroy]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
@@ -62,11 +62,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
:auto_renew?
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :all_active do
|
||||
prepare build(sort: [updated_at: :asc], load: [:map])
|
||||
@@ -92,39 +88,32 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
update :update_plan do
|
||||
accept [:plan]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_characters_limit do
|
||||
accept [:characters_limit]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_hubs_limit do
|
||||
accept [:hubs_limit]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_active_till do
|
||||
accept [:active_till]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_auto_renew do
|
||||
accept [:auto_renew?]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :cancel do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:status, :cancelled))
|
||||
end
|
||||
|
||||
update :expire do
|
||||
accept([])
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:status, :expired))
|
||||
end
|
||||
|
||||
@@ -24,12 +24,16 @@ defmodule WandererApp.Api.MapSystem do
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshJsonApi.Resource],
|
||||
primary_read_warning?: false
|
||||
extensions: [AshJsonApi.Resource]
|
||||
|
||||
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
|
||||
@@ -66,7 +70,10 @@ defmodule WandererApp.Api.MapSystem do
|
||||
define(:upsert, action: :upsert)
|
||||
define(:destroy, action: :destroy)
|
||||
|
||||
define :by_id, action: :get_by_id, args: [:id], get?: true
|
||||
define(:by_id,
|
||||
get_by: [:id],
|
||||
action: :read
|
||||
)
|
||||
|
||||
define(:by_solar_system_id,
|
||||
get_by: [:solar_system_id],
|
||||
@@ -96,7 +103,6 @@ defmodule WandererApp.Api.MapSystem do
|
||||
define(:update_status, action: :update_status)
|
||||
define(:update_tag, action: :update_tag)
|
||||
define(:update_temporary_name, action: :update_temporary_name)
|
||||
define(:update_custom_name, action: :update_custom_name)
|
||||
define(:update_labels, action: :update_labels)
|
||||
define(:update_linked_sig_eve_id, action: :update_linked_sig_eve_id)
|
||||
define(:update_position, action: :update_position)
|
||||
@@ -122,56 +128,7 @@ defmodule WandererApp.Api.MapSystem do
|
||||
:linked_sig_eve_id
|
||||
]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
accept [
|
||||
: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
|
||||
]
|
||||
|
||||
# Inject map_id from token
|
||||
change WandererApp.Api.Changes.InjectMapFromActor
|
||||
end
|
||||
|
||||
update :update do
|
||||
primary? true
|
||||
require_atomic? false
|
||||
|
||||
# Note: name and solar_system_id are not in accept
|
||||
# - solar_system_id should be immutable (identifier)
|
||||
# - name has allow_nil? false which makes it required in JSON:API
|
||||
accept [
|
||||
:position_x,
|
||||
:position_y,
|
||||
:status,
|
||||
:visible,
|
||||
:locked,
|
||||
:custom_name,
|
||||
:description,
|
||||
:tag,
|
||||
:temporary_name,
|
||||
:labels,
|
||||
:linked_sig_eve_id
|
||||
]
|
||||
end
|
||||
|
||||
destroy :destroy do
|
||||
primary? true
|
||||
end
|
||||
defaults [:create, :update, :destroy]
|
||||
|
||||
create :upsert do
|
||||
primary? false
|
||||
@@ -201,9 +158,6 @@ defmodule WandererApp.Api.MapSystem do
|
||||
read :read do
|
||||
primary?(true)
|
||||
|
||||
# Security: Filter to only systems from actor's map
|
||||
prepare WandererApp.Api.Preparations.FilterSystemsByActorMap
|
||||
|
||||
pagination offset?: true,
|
||||
default_limit: 100,
|
||||
max_page_size: 500,
|
||||
@@ -211,11 +165,6 @@ defmodule WandererApp.Api.MapSystem do
|
||||
required?: false
|
||||
end
|
||||
|
||||
read :get_by_id do
|
||||
argument(:id, :string, allow_nil?: false)
|
||||
filter(expr(id == ^arg(:id)))
|
||||
end
|
||||
|
||||
read :read_all_by_map do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
filter(expr(map_id == ^arg(:map_id)))
|
||||
@@ -237,59 +186,44 @@ defmodule WandererApp.Api.MapSystem do
|
||||
|
||||
update :update_name do
|
||||
accept [:name]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_description do
|
||||
accept [:description]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_locked do
|
||||
accept [:locked]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_status do
|
||||
accept [:status]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_tag do
|
||||
accept [:tag]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_temporary_name do
|
||||
accept [:temporary_name]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_custom_name do
|
||||
accept [:custom_name]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_labels do
|
||||
accept [:labels]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_position do
|
||||
accept [:position_x, :position_y]
|
||||
require_atomic? false
|
||||
|
||||
change(set_attribute(:visible, true))
|
||||
end
|
||||
|
||||
update :update_linked_sig_eve_id do
|
||||
accept [:linked_sig_eve_id]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_visible do
|
||||
accept [:visible]
|
||||
require_atomic? false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
:character_id,
|
||||
:text
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
argument :character_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
read :by_system_id do
|
||||
|
||||
@@ -111,6 +111,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:custom_info,
|
||||
:deleted
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
update :update do
|
||||
@@ -135,17 +139,14 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
update :update_linked_system do
|
||||
accept [:linked_system_id]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_type do
|
||||
accept [:type]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_group do
|
||||
accept [:group]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
read :by_system_id do
|
||||
|
||||
@@ -122,6 +122,13 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:system_id, :system,
|
||||
on_lookup: :relate,
|
||||
on_no_match: nil
|
||||
)
|
||||
end
|
||||
|
||||
update :update do
|
||||
|
||||
@@ -29,11 +29,7 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
:amount
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :by_map do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
|
||||
@@ -53,30 +53,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
:settings
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
update :update_settings do
|
||||
accept [:settings]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_main_character do
|
||||
accept [:main_character_eve_id]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_following_character do
|
||||
accept [:following_character_eve_id]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_hubs do
|
||||
accept [:hubs]
|
||||
require_atomic? false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -58,8 +58,6 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
||||
:consecutive_failures,
|
||||
:secret
|
||||
]
|
||||
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
read :by_map do
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
defmodule WandererApp.Api.Preparations.FilterByActorMap do
|
||||
@moduledoc """
|
||||
Shared filtering logic for actor map context.
|
||||
|
||||
Filters queries to only return resources belonging to the actor's map.
|
||||
Used by preparations for MapSystem, MapConnection, and MapPing resources.
|
||||
"""
|
||||
|
||||
require Ash.Query
|
||||
|
||||
alias WandererApp.Api.ActorHelpers
|
||||
|
||||
@doc """
|
||||
Filter a query by the actor's map context.
|
||||
|
||||
If a map is found in the context, filters the query to only return
|
||||
resources where map_id matches. If no map context exists, returns
|
||||
a query that will return no results.
|
||||
|
||||
## Parameters
|
||||
|
||||
* `query` - The Ash query to filter
|
||||
* `context` - The Ash context containing actor/map information
|
||||
* `resource_name` - Name of the resource for telemetry (atom)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> query = Ash.Query.new(WandererApp.Api.MapSystem)
|
||||
iex> context = %{map: %{id: "map-123"}}
|
||||
iex> result = FilterByActorMap.filter_by_map(query, context, :map_system)
|
||||
# Returns query filtered by map_id == "map-123"
|
||||
"""
|
||||
def filter_by_map(query, context, resource_name) do
|
||||
case ActorHelpers.get_map(context) do
|
||||
%{id: map_id} ->
|
||||
emit_telemetry(resource_name, map_id)
|
||||
Ash.Query.filter(query, map_id == ^map_id)
|
||||
|
||||
nil ->
|
||||
emit_telemetry_no_context(resource_name)
|
||||
Ash.Query.filter(query, false)
|
||||
|
||||
_other ->
|
||||
emit_telemetry_no_context(resource_name)
|
||||
Ash.Query.filter(query, false)
|
||||
end
|
||||
end
|
||||
|
||||
defp emit_telemetry(resource_name, map_id) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :ash, :preparation, :filter_by_map],
|
||||
%{count: 1},
|
||||
%{resource: resource_name, map_id: map_id}
|
||||
)
|
||||
end
|
||||
|
||||
defp emit_telemetry_no_context(resource_name) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :ash, :preparation, :filter_by_map, :no_context],
|
||||
%{count: 1},
|
||||
%{resource: resource_name}
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
defmodule WandererApp.Api.Preparations.FilterConnectionsByActorMap do
|
||||
@moduledoc """
|
||||
Ash preparation that filters connections to only those from the actor's map.
|
||||
|
||||
For token-based auth, this ensures the API only returns connections
|
||||
from the map associated with the token.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
||||
|
||||
@impl true
|
||||
def prepare(query, _opts, context) do
|
||||
FilterByActorMap.filter_by_map(query, context, :map_connection)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
defmodule WandererApp.Api.Preparations.FilterPingsByActorMap do
|
||||
@moduledoc """
|
||||
Ash preparation that filters pings to only those from the actor's map.
|
||||
|
||||
For token-based auth, this ensures the API only returns pings
|
||||
from the map associated with the token.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
||||
|
||||
@impl true
|
||||
def prepare(query, _opts, context) do
|
||||
FilterByActorMap.filter_by_map(query, context, :map_ping)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
defmodule WandererApp.Api.Preparations.FilterSystemsByActorMap do
|
||||
@moduledoc """
|
||||
Ash preparation that filters systems to only those from the actor's map.
|
||||
|
||||
For token-based auth, this ensures the API only returns systems
|
||||
from the map associated with the token.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
||||
|
||||
@impl true
|
||||
def prepare(query, _opts, context) do
|
||||
FilterByActorMap.filter_by_map(query, context, :map_system)
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
defmodule WandererApp.Api.Preparations.SecureApiKeyLookup do
|
||||
@moduledoc """
|
||||
Preparation that performs secure API key lookup using constant-time comparison.
|
||||
|
||||
This preparation:
|
||||
1. Queries for the map with the given API key using database index
|
||||
2. Performs constant-time comparison to verify the key matches
|
||||
3. Returns the map only if the secure comparison passes
|
||||
|
||||
The constant-time comparison prevents timing attacks where an attacker
|
||||
could deduce information about valid API keys by measuring response times.
|
||||
"""
|
||||
|
||||
use Ash.Resource.Preparation
|
||||
require Ash.Query
|
||||
|
||||
@dummy_key "dummy_key_for_timing_consistency_00000000"
|
||||
|
||||
def prepare(query, _params, _context) do
|
||||
api_key = Ash.Query.get_argument(query, :api_key)
|
||||
|
||||
if is_nil(api_key) or api_key == "" do
|
||||
# Return empty result for invalid input
|
||||
Ash.Query.filter(query, expr(false))
|
||||
else
|
||||
# First, do the database lookup using the index
|
||||
# Then apply constant-time comparison in after_action
|
||||
query
|
||||
|> Ash.Query.filter(expr(public_api_key == ^api_key))
|
||||
|> Ash.Query.after_action(fn _query, results ->
|
||||
verify_results_with_secure_compare(results, api_key)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_results_with_secure_compare(results, provided_key) do
|
||||
case results do
|
||||
[map] ->
|
||||
# Map found - verify with constant-time comparison
|
||||
stored_key = map.public_api_key || @dummy_key
|
||||
|
||||
if Plug.Crypto.secure_compare(stored_key, provided_key) do
|
||||
{:ok, [map]}
|
||||
else
|
||||
# Keys don't match (shouldn't happen if DB returned it, but safety check)
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
[] ->
|
||||
# No map found - still do a comparison to maintain consistent timing
|
||||
# This prevents timing attacks from distinguishing "not found" from "found but wrong"
|
||||
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
|
||||
{:ok, []}
|
||||
|
||||
_multiple ->
|
||||
# Multiple results - shouldn't happen with unique constraint
|
||||
# Do comparison for timing consistency and return error
|
||||
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -49,11 +49,7 @@ defmodule WandererApp.Api.ShipTypeInfo do
|
||||
:volume
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:read, :destroy, :update]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
@@ -51,15 +51,10 @@ defmodule WandererApp.Api.User do
|
||||
:hash
|
||||
]
|
||||
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
require_atomic? false
|
||||
end
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
update :update_last_map do
|
||||
accept([:last_map_id])
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_balance do
|
||||
|
||||
@@ -4,8 +4,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshJsonApi.Resource],
|
||||
primary_read_warning?: false
|
||||
extensions: [AshJsonApi.Resource]
|
||||
|
||||
require Ash.Expr
|
||||
|
||||
@@ -56,8 +55,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
:entity_type,
|
||||
:event_type,
|
||||
:event_data,
|
||||
:user_id,
|
||||
:character_id
|
||||
:user_id
|
||||
]
|
||||
|
||||
read :read do
|
||||
@@ -72,8 +70,14 @@ defmodule WandererApp.Api.UserActivity do
|
||||
end
|
||||
|
||||
create :new do
|
||||
accept [:entity_id, :entity_type, :event_type, :event_data, :user_id, :character_id]
|
||||
accept [:entity_id, :entity_type, :event_type, :event_data]
|
||||
primary?(true)
|
||||
|
||||
argument :user_id, :uuid, allow_nil?: true
|
||||
argument :character_id, :uuid, allow_nil?: true
|
||||
|
||||
change manage_relationship(:user_id, :user, on_lookup: :relate, on_no_match: nil)
|
||||
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
|
||||
destroy :archive do
|
||||
|
||||
@@ -28,6 +28,10 @@ defmodule WandererApp.Api.UserTransaction do
|
||||
create :new do
|
||||
accept [:journal_ref_id, :user_id, :date, :amount, :corporation_id]
|
||||
primary?(true)
|
||||
|
||||
argument :user_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:user_id, :user, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -153,16 +153,13 @@ defmodule WandererApp.Application do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp maybe_start_corp_wallet_tracker(true) do
|
||||
# Don't start corp wallet tracker in test environment
|
||||
if Application.get_env(:wanderer_app, :environment) == :test do
|
||||
[]
|
||||
else
|
||||
[WandererApp.StartCorpWalletTrackerTask]
|
||||
end
|
||||
end
|
||||
defp maybe_start_corp_wallet_tracker(true),
|
||||
do: [
|
||||
WandererApp.StartCorpWalletTrackerTask
|
||||
]
|
||||
|
||||
defp maybe_start_corp_wallet_tracker(_), do: []
|
||||
defp maybe_start_corp_wallet_tracker(_),
|
||||
do: []
|
||||
|
||||
defp maybe_start_kills_services do
|
||||
# Don't start kills services in test environment
|
||||
|
||||
@@ -93,8 +93,6 @@ defmodule WandererApp.CachedInfo do
|
||||
end
|
||||
end
|
||||
|
||||
def get_system_static_info(nil), do: {:ok, nil}
|
||||
|
||||
def get_system_static_info(solar_system_id) do
|
||||
{:ok, solar_system_id} = APIUtils.parse_int(solar_system_id)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ defmodule WandererApp.Env do
|
||||
def invites(), do: get_key(:invites, false)
|
||||
|
||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
|
||||
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
|
||||
|
||||
@decorate cacheable(
|
||||
|
||||
@@ -155,23 +155,26 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
|
||||
# 1. Store in ETS for backfill
|
||||
store_event(event, state.ets_table)
|
||||
|
||||
# 2. Convert event to JSON for delivery methods
|
||||
event_json = Event.to_json(event)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"MapEventRelay converted event to JSON: #{inspect(String.slice(inspect(event_json), 0, 200))}..."
|
||||
end)
|
||||
|
||||
# 3. Send to webhook subscriptions via WebhookDispatcher
|
||||
WebhookDispatcher.dispatch_event(event.map_id, event)
|
||||
|
||||
case WandererApp.ExternalEvents.SseAccessControl.sse_allowed?(event.map_id) do
|
||||
:ok ->
|
||||
WandererApp.ExternalEvents.SseStreamManager.broadcast_event(event.map_id, event_json)
|
||||
# 4. Broadcast to SSE clients
|
||||
Logger.debug(fn -> "MapEventRelay broadcasting to SSE clients for map #{event.map_id}" end)
|
||||
WandererApp.ExternalEvents.SseStreamManager.broadcast_event(event.map_id, event_json)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :external_events, :relay, :delivered],
|
||||
%{count: 1},
|
||||
%{map_id: event.map_id, event_type: event.type}
|
||||
)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
# Emit delivered telemetry
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :external_events, :relay, :delivered],
|
||||
%{count: 1},
|
||||
%{map_id: event.map_id, event_type: event.type}
|
||||
)
|
||||
|
||||
%{state | event_count: state.event_count + 1}
|
||||
end
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
defmodule WandererApp.ExternalEvents.SseAccessControl do
|
||||
@moduledoc """
|
||||
Handles SSE access control checks including subscription validation.
|
||||
|
||||
Note: Community Edition mode is automatically handled by the
|
||||
WandererApp.Map.is_subscription_active?/1 function, which returns
|
||||
{:ok, true} when subscriptions are disabled globally.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Checks if SSE is allowed for a given map.
|
||||
|
||||
Returns:
|
||||
- :ok if SSE is allowed
|
||||
- {:error, reason} if SSE is not allowed
|
||||
|
||||
Checks in order:
|
||||
1. Global SSE enabled (config)
|
||||
2. Map exists
|
||||
3. Map SSE enabled (per-map setting)
|
||||
4. Subscription active (CE mode handled internally)
|
||||
"""
|
||||
def sse_allowed?(map_id) do
|
||||
with :ok <- check_sse_globally_enabled(),
|
||||
{:ok, map} <- fetch_map(map_id),
|
||||
:ok <- check_map_sse_enabled(map),
|
||||
:ok <- check_subscription_or_ce(map_id) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp check_sse_globally_enabled do
|
||||
if WandererApp.Env.sse_enabled?() do
|
||||
:ok
|
||||
else
|
||||
{:error, :sse_globally_disabled}
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the map by ID.
|
||||
# Returns {:ok, map} or {:error, :map_not_found}
|
||||
defp fetch_map(map_id) do
|
||||
case WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, _map} = result -> result
|
||||
_ -> {:error, :map_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_map_sse_enabled(map) do
|
||||
if map.sse_enabled do
|
||||
:ok
|
||||
else
|
||||
{:error, :sse_disabled_for_map}
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if map has active subscription or if running Community Edition.
|
||||
#
|
||||
# Returns :ok if:
|
||||
# - Community Edition (handled internally by is_subscription_active?/1), OR
|
||||
# - Map has active subscription
|
||||
#
|
||||
# Returns {:error, :subscription_required} if subscription check fails.
|
||||
defp check_subscription_or_ce(map_id) do
|
||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
||||
{:ok, true} -> :ok
|
||||
{:ok, false} -> {:error, :subscription_required}
|
||||
{:error, _reason} = error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -403,24 +403,10 @@ defmodule WandererApp.Kills.MessageHandler do
|
||||
|
||||
defp extract_field(_data, _field_names), do: nil
|
||||
|
||||
# Generic nested field extraction - tries flat keys first, then nested object
|
||||
@spec extract_nested_field(map(), list(String.t()), String.t(), String.t()) :: String.t() | nil
|
||||
defp extract_nested_field(data, flat_keys, nested_key, field) when is_map(data) do
|
||||
case extract_field(data, flat_keys) do
|
||||
nil ->
|
||||
case data[nested_key] do
|
||||
%{^field => value} when is_binary(value) and value != "" -> value
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Specific field extractors using the generic functions
|
||||
# Specific field extractors using the generic function
|
||||
@spec get_character_name(map() | any()) :: String.t() | nil
|
||||
defp get_character_name(data) when is_map(data) do
|
||||
# Try multiple possible field names
|
||||
field_names = ["attacker_name", "victim_name", "character_name", "name"]
|
||||
|
||||
extract_field(data, field_names) ||
|
||||
@@ -433,26 +419,30 @@ defmodule WandererApp.Kills.MessageHandler do
|
||||
defp get_character_name(_), do: nil
|
||||
|
||||
@spec get_corp_ticker(map() | any()) :: String.t() | nil
|
||||
defp get_corp_ticker(data) when is_map(data),
|
||||
do: extract_nested_field(data, ["corporation_ticker", "corp_ticker"], "corporation", "ticker")
|
||||
defp get_corp_ticker(data) when is_map(data) do
|
||||
extract_field(data, ["corporation_ticker", "corp_ticker"])
|
||||
end
|
||||
|
||||
defp get_corp_ticker(_), do: nil
|
||||
|
||||
@spec get_corp_name(map() | any()) :: String.t() | nil
|
||||
defp get_corp_name(data) when is_map(data),
|
||||
do: extract_nested_field(data, ["corporation_name", "corp_name"], "corporation", "name")
|
||||
defp get_corp_name(data) when is_map(data) do
|
||||
extract_field(data, ["corporation_name", "corp_name"])
|
||||
end
|
||||
|
||||
defp get_corp_name(_), do: nil
|
||||
|
||||
@spec get_alliance_ticker(map() | any()) :: String.t() | nil
|
||||
defp get_alliance_ticker(data) when is_map(data),
|
||||
do: extract_nested_field(data, ["alliance_ticker"], "alliance", "ticker")
|
||||
defp get_alliance_ticker(data) when is_map(data) do
|
||||
extract_field(data, ["alliance_ticker"])
|
||||
end
|
||||
|
||||
defp get_alliance_ticker(_), do: nil
|
||||
|
||||
@spec get_alliance_name(map() | any()) :: String.t() | nil
|
||||
defp get_alliance_name(data) when is_map(data),
|
||||
do: extract_nested_field(data, ["alliance_name"], "alliance", "name")
|
||||
defp get_alliance_name(data) when is_map(data) do
|
||||
extract_field(data, ["alliance_name"])
|
||||
end
|
||||
|
||||
defp get_alliance_name(_), do: nil
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ defmodule WandererApp.Map do
|
||||
|
||||
characters_ids =
|
||||
characters
|
||||
|> Enum.map(fn %{character_id: char_id} -> char_id end)
|
||||
|> Enum.map(fn %{id: char_id} -> char_id end)
|
||||
|
||||
# Filter out characters that already exist
|
||||
new_character_ids =
|
||||
|
||||
@@ -348,9 +348,9 @@ defmodule WandererApp.Map.CacheRTree do
|
||||
[{x1_min, x1_max}, {y1_min, y1_max}] = box1
|
||||
[{x2_min, x2_max}, {y2_min, y2_max}] = box2
|
||||
|
||||
# Boxes intersect if they overlap on both axes (strict intersection - not just touching)
|
||||
x_overlap = x1_min < x2_max and x2_min < x1_max
|
||||
y_overlap = y1_min < y2_max and y2_min < y1_max
|
||||
# Boxes intersect if they overlap on both axes
|
||||
x_overlap = x1_min <= x2_max and x2_min <= x1_max
|
||||
y_overlap = y1_min <= y2_max and y2_min <= y1_max
|
||||
|
||||
x_overlap and y_overlap
|
||||
end
|
||||
|
||||
@@ -18,20 +18,10 @@ defmodule WandererApp.Map.MapPool do
|
||||
@map_pool_limit 10
|
||||
|
||||
@garbage_collection_interval :timer.hours(4)
|
||||
# Use very long timeouts in test environment to prevent background tasks from running during tests
|
||||
# This avoids database connection ownership errors when tests finish before async tasks complete
|
||||
@systems_cleanup_timeout if Mix.env() == :test,
|
||||
do: :timer.hours(24),
|
||||
else: :timer.minutes(30)
|
||||
@characters_cleanup_timeout if Mix.env() == :test,
|
||||
do: :timer.hours(24),
|
||||
else: :timer.minutes(5)
|
||||
@connections_cleanup_timeout if Mix.env() == :test,
|
||||
do: :timer.hours(24),
|
||||
else: :timer.minutes(5)
|
||||
@backup_state_timeout if Mix.env() == :test,
|
||||
do: :timer.hours(24),
|
||||
else: :timer.minutes(1)
|
||||
@systems_cleanup_timeout :timer.minutes(30)
|
||||
@characters_cleanup_timeout :timer.minutes(5)
|
||||
@connections_cleanup_timeout :timer.minutes(5)
|
||||
@backup_state_timeout :timer.minutes(1)
|
||||
|
||||
def new(), do: __struct__()
|
||||
def new(args), do: __struct__(args)
|
||||
@@ -197,7 +187,7 @@ defmodule WandererApp.Map.MapPool do
|
||||
|
||||
# Schedule periodic tasks
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
Process.send_after(self(), :cleanup_systems, @systems_cleanup_timeout)
|
||||
Process.send_after(self(), :cleanup_systems, 15_000)
|
||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||
Process.send_after(self(), :cleanup_connections, @connections_cleanup_timeout)
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
|
||||
@@ -106,9 +106,6 @@ defmodule WandererApp.Map.PositionCalculator do
|
||||
|
||||
defp get_start_index(n, "top_to_bottom"), do: div(n, 2) + n - 1
|
||||
|
||||
# Default to left_to_right when layout is nil
|
||||
defp get_start_index(n, nil), do: div(n, 2)
|
||||
|
||||
defp adjusted_coordinates(n, start_x, start_y, opts) when n > 1 do
|
||||
sorted_coords = sorted_edge_coordinates(n, opts)
|
||||
|
||||
|
||||
@@ -56,8 +56,6 @@ defmodule WandererApp.Map.Server do
|
||||
|
||||
defdelegate update_system_temporary_name(map_id, update), to: Impl
|
||||
|
||||
defdelegate update_system_custom_name(map_id, update), to: Impl
|
||||
|
||||
defdelegate update_system_locked(map_id, update), to: Impl
|
||||
|
||||
defdelegate update_system_labels(map_id, update), to: Impl
|
||||
|
||||
@@ -72,7 +72,7 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
Logger.debug("Copying systems for map #{source_map.id}")
|
||||
|
||||
# Get all systems from source map using Ash
|
||||
case MapSystem.read_all_by_map(%{map_id: source_map.id}) do
|
||||
case MapSystem |> Ash.Query.filter(map_id == ^source_map.id) |> Ash.read() do
|
||||
{:ok, source_systems} ->
|
||||
system_mapping = %{}
|
||||
|
||||
@@ -126,7 +126,7 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
defp copy_connections(source_map, new_map, system_mapping) do
|
||||
Logger.debug("Copying connections for map #{source_map.id}")
|
||||
|
||||
case MapConnection.read_by_map(%{map_id: source_map.id}) do
|
||||
case MapConnection |> Ash.Query.filter(map_id == ^source_map.id) |> Ash.read() do
|
||||
{:ok, source_connections} ->
|
||||
Enum.reduce_while(source_connections, {:ok, []}, fn source_connection,
|
||||
{:ok, acc_connections} ->
|
||||
@@ -222,7 +222,7 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
source_system_ids = Map.keys(system_mapping)
|
||||
|
||||
Enum.flat_map(source_system_ids, fn system_id ->
|
||||
case MapSystemSignature.by_system_id_all(%{system_id: system_id}) do
|
||||
case MapSystemSignature |> Ash.Query.filter(system_id == ^system_id) |> Ash.read() do
|
||||
{:ok, signatures} -> signatures
|
||||
{:error, _} -> []
|
||||
end
|
||||
@@ -355,7 +355,7 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
defp maybe_copy_user_settings(source_map, new_map, true) do
|
||||
Logger.debug("Copying user settings for map #{source_map.id}")
|
||||
|
||||
case MapCharacterSettings.read_by_map(%{map_id: source_map.id}) do
|
||||
case MapCharacterSettings |> Ash.Query.filter(map_id == ^source_map.id) |> Ash.read() do
|
||||
{:ok, source_settings} ->
|
||||
Enum.reduce_while(source_settings, {:ok, []}, fn source_setting, {:ok, acc_settings} ->
|
||||
case copy_single_character_setting(source_setting, new_map.id) do
|
||||
|
||||
@@ -8,38 +8,35 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
|
||||
# Private helper to validate character_eve_id from params 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} | {:error, :unexpected_error}
|
||||
{: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, %Ash.Error.Query.NotFound{}} ->
|
||||
_ ->
|
||||
{:error, :invalid_character}
|
||||
|
||||
{:error, %Ash.Error.Invalid{}} ->
|
||||
# Invalid format (e.g., non-numeric string for an integer field)
|
||||
{:error, :invalid_character}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"[validate_character_eve_id] Unexpected error looking up character: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :unexpected_error}
|
||||
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
|
||||
@@ -77,8 +74,12 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(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("system_id", system.id)
|
||||
@@ -89,6 +90,7 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
# Pass internal UUID here
|
||||
character_id: validated_char_uuid,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
@@ -125,10 +127,6 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
Logger.error("[create_signature] Invalid character_eve_id provided")
|
||||
{:error, :invalid_character}
|
||||
|
||||
{:error, :unexpected_error} ->
|
||||
Logger.error("[create_signature] Unexpected error during character validation")
|
||||
{:error, :unexpected_error}
|
||||
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
@@ -154,6 +152,8 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
sig_id,
|
||||
params
|
||||
) do
|
||||
# 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
|
||||
@@ -177,6 +177,7 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
updated_signatures: [attrs],
|
||||
removed_signatures: [],
|
||||
solar_system_id: system.solar_system_id,
|
||||
# Pass internal UUID here
|
||||
character_id: validated_char_uuid,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
@@ -199,13 +200,9 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
Logger.error("[update_signature] Invalid character_eve_id provided")
|
||||
{:error, :invalid_character}
|
||||
|
||||
{:error, :unexpected_error} ->
|
||||
Logger.error("[update_signature] Unexpected error during character validation")
|
||||
{:error, :unexpected_error}
|
||||
|
||||
err ->
|
||||
Logger.error("[update_signature] Signature or system not found: #{inspect(err)}")
|
||||
{:error, :not_found}
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -35,22 +35,21 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
# Private helper for batch upsert
|
||||
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
||||
with {:ok, solar_system_id} <- fetch_system_id(params) do
|
||||
update_existing = fetch_update_existing(params, false)
|
||||
{:ok, solar_system_id} = fetch_system_id(params)
|
||||
update_existing = fetch_update_existing(params, false)
|
||||
|
||||
map_id
|
||||
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|
||||
|> case do
|
||||
{:ok, _location} ->
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
map_id
|
||||
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|
||||
|> case do
|
||||
{:ok, _location} ->
|
||||
do_create_system(map_id, user_id, char_id, params)
|
||||
|
||||
{:error, :already_exists} ->
|
||||
if update_existing do
|
||||
do_update_system(map_id, user_id, char_id, solar_system_id, params)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
{:error, :already_exists} ->
|
||||
if update_existing do
|
||||
do_update_system(map_id, user_id, char_id, solar_system_id, params)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,8 +106,8 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
Logger.warning("[update_system] Expected error: #{inspect(reason)}")
|
||||
{:error, :expected_error}
|
||||
|
||||
error ->
|
||||
Logger.error("[update_system] Unexpected error: #{inspect(error)}")
|
||||
_ ->
|
||||
Logger.error("[update_system] Unexpected error")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
@@ -186,8 +185,6 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
|
||||
|
||||
defp parse_int(val, _field) when is_float(val), do: {:ok, trunc(val)}
|
||||
|
||||
defp parse_int(val, field) when is_binary(val) do
|
||||
case Integer.parse(val) do
|
||||
{i, _} -> {:ok, i}
|
||||
@@ -271,9 +268,12 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
})
|
||||
|
||||
"custom_name" ->
|
||||
Server.update_system_custom_name(map_id, %{
|
||||
{:ok, solar_system_info} =
|
||||
WandererApp.CachedInfo.get_system_static_info(system_id)
|
||||
|
||||
Server.update_system_name(map_id, %{
|
||||
solar_system_id: system_id,
|
||||
custom_name: val
|
||||
name: val || solar_system_info.solar_system_name
|
||||
})
|
||||
|
||||
"temporary_name" ->
|
||||
|
||||
@@ -21,7 +21,6 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
:map_id,
|
||||
:rtree_name,
|
||||
map: nil,
|
||||
acls: [],
|
||||
map_opts: []
|
||||
]
|
||||
|
||||
@@ -45,12 +44,6 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
}
|
||||
|> new()
|
||||
|
||||
# In test mode, give the test setup time to grant database access
|
||||
# This is necessary for async tests where the sandbox needs to allow this process
|
||||
if Mix.env() == :test do
|
||||
Process.sleep(150)
|
||||
end
|
||||
|
||||
# Parallelize database queries for faster initialization
|
||||
start_time = System.monotonic_time(:millisecond)
|
||||
|
||||
@@ -58,15 +51,14 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Task.async(fn ->
|
||||
{:map,
|
||||
WandererApp.MapRepo.get(map_id, [
|
||||
:owner
|
||||
:owner,
|
||||
:characters,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
])}
|
||||
end),
|
||||
Task.async(fn ->
|
||||
{:acls, WandererApp.Api.MapAccessList.read_by_map(%{map_id: map_id})}
|
||||
end),
|
||||
Task.async(fn ->
|
||||
{:characters, WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)}
|
||||
end),
|
||||
Task.async(fn ->
|
||||
{:systems, WandererApp.MapSystemRepo.get_visible_by_map(map_id)}
|
||||
end),
|
||||
@@ -100,18 +92,6 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
acls_result =
|
||||
Enum.find_value(results, fn
|
||||
{:acls, result} -> result
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
characters_result =
|
||||
Enum.find_value(results, fn
|
||||
{:characters, result} -> result
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
systems_result =
|
||||
Enum.find_value(results, fn
|
||||
{:systems, result} -> result
|
||||
@@ -132,16 +112,12 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
# Process results
|
||||
with {:ok, map} <- map_result,
|
||||
{:ok, acls} <- acls_result,
|
||||
{:ok, characters} <- characters_result,
|
||||
{:ok, systems} <- systems_result,
|
||||
{:ok, connections} <- connections_result,
|
||||
{:ok, subscription_settings} <- subscription_result do
|
||||
initial_state
|
||||
|> init_map(
|
||||
map,
|
||||
acls,
|
||||
characters,
|
||||
subscription_settings,
|
||||
systems,
|
||||
connections
|
||||
@@ -153,7 +129,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
end
|
||||
end
|
||||
|
||||
def start_map(%__MODULE__{map: map, acls: acls, map_id: map_id} = _state) do
|
||||
def start_map(%__MODULE__{map: map, map_id: map_id} = _state) do
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", false)
|
||||
|
||||
# Check if map was loaded successfully
|
||||
@@ -163,7 +139,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:error, :map_not_loaded}
|
||||
|
||||
map ->
|
||||
with :ok <- AclsImpl.track_acls(acls |> Enum.map(& &1.access_list_id)) do
|
||||
with :ok <- AclsImpl.track_acls(map.acls |> Enum.map(& &1.id)) do
|
||||
@pubsub_client.subscribe(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{map_id}"
|
||||
@@ -243,7 +219,6 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
defdelegate update_system_status(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_tag(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_temporary_name(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_custom_name(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_locked(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_labels(map_id, update), to: SystemsImpl
|
||||
defdelegate update_system_linked_sig_eve_id(map_id, update), to: SystemsImpl
|
||||
@@ -313,57 +288,12 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
acc |> Map.put_new(connection_id, connection_start_time)
|
||||
end)
|
||||
|
||||
# Create map state with retry logic for test scenarios
|
||||
create_map_state_with_retry(
|
||||
%{
|
||||
map_id: map_id,
|
||||
systems_last_activity: systems_last_activity,
|
||||
connections_eol_time: connections_eol_time,
|
||||
connections_start_time: connections_start_time
|
||||
},
|
||||
3
|
||||
)
|
||||
end
|
||||
|
||||
# Helper to create map state with retry logic for async tests
|
||||
defp create_map_state_with_retry(attrs, retries_left) when retries_left > 0 do
|
||||
case WandererApp.Api.MapState.create(attrs) do
|
||||
{:ok, map_state} = result ->
|
||||
result
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} = error ->
|
||||
# Check if it's a foreign key constraint error
|
||||
has_fkey_error =
|
||||
Enum.any?(errors, fn
|
||||
%Ash.Error.Changes.InvalidAttribute{private_vars: private_vars} ->
|
||||
Enum.any?(private_vars, fn
|
||||
{:constraint_type, :foreign_key} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
if has_fkey_error and retries_left > 1 do
|
||||
# In test environments with async tests, the parent map might not be
|
||||
# visible yet due to sandbox timing. Brief retry with exponential backoff.
|
||||
sleep_time = (4 - retries_left) * 15 + 10
|
||||
Process.sleep(sleep_time)
|
||||
create_map_state_with_retry(attrs, retries_left - 1)
|
||||
else
|
||||
# Return error if not a foreign key issue or out of retries
|
||||
error
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp create_map_state_with_retry(attrs, 0) do
|
||||
# Final attempt without retry
|
||||
WandererApp.Api.MapState.create(attrs)
|
||||
WandererApp.Api.MapState.create(%{
|
||||
map_id: map_id,
|
||||
systems_last_activity: systems_last_activity,
|
||||
connections_eol_time: connections_eol_time,
|
||||
connections_start_time: connections_start_time
|
||||
})
|
||||
end
|
||||
|
||||
def handle_event({:update_characters, map_id} = event) do
|
||||
@@ -550,9 +480,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
defp init_map(
|
||||
state,
|
||||
%{id: map_id} = initial_map,
|
||||
acls,
|
||||
characters,
|
||||
%{id: map_id, characters: characters} = initial_map,
|
||||
subscription_settings,
|
||||
systems,
|
||||
connections
|
||||
@@ -581,7 +509,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
|
||||
|
||||
%{state | map: map, acls: acls, map_opts: map_options(options)}
|
||||
%{state | map: map, map_opts: map_options(options)}
|
||||
end
|
||||
|
||||
def maybe_import_systems(
|
||||
|
||||
@@ -106,7 +106,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
) do
|
||||
system =
|
||||
WandererApp.Map.find_system_by_location(map_id, %{
|
||||
solar_system_id: solar_system_id
|
||||
solar_system_id: solar_system_id |> String.to_integer()
|
||||
})
|
||||
|
||||
{:ok, comment} =
|
||||
@@ -118,7 +118,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|
||||
comment =
|
||||
comment
|
||||
|> Ash.load!([:character])
|
||||
|> Ash.load!([:character, :system])
|
||||
|
||||
Impl.broadcast!(map_id, :system_comment_added, %{
|
||||
solar_system_id: solar_system_id,
|
||||
@@ -132,11 +132,9 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
user_id,
|
||||
character_id
|
||||
) do
|
||||
{:ok, %{system_id: system_id} = comment} =
|
||||
{:ok, %{system: system} = comment} =
|
||||
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
|
||||
|
||||
{:ok, system} = WandererApp.Api.MapSystem.by_id(system_id)
|
||||
|
||||
:ok = WandererApp.MapSystemCommentRepo.destroy(comment)
|
||||
|
||||
Impl.broadcast!(map_id, :system_comment_removed, %{
|
||||
@@ -215,12 +213,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
),
|
||||
do: update_system(map_id, :update_temporary_name, [:temporary_name], update)
|
||||
|
||||
def update_system_custom_name(
|
||||
map_id,
|
||||
update
|
||||
),
|
||||
do: update_system(map_id, :update_custom_name, [:custom_name], update)
|
||||
|
||||
def update_system_locked(
|
||||
map_id,
|
||||
update
|
||||
@@ -655,135 +647,104 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
user_id,
|
||||
character_id
|
||||
) do
|
||||
# Verify the map exists in the database before attempting to create a system
|
||||
# This prevents foreign key constraint errors when tests roll back transactions
|
||||
with {:ok, _map} <- WandererApp.MapRepo.get(map_id),
|
||||
{:ok, %{map_opts: map_opts}} <- WandererApp.Map.get_map_state(map_id) do
|
||||
extra_info = system_info |> Map.get(:extra_info)
|
||||
rtree_name = "rtree_#{map_id}"
|
||||
extra_info = system_info |> Map.get(:extra_info)
|
||||
rtree_name = "rtree_#{map_id}"
|
||||
{:ok, %{map_opts: map_opts}} = WandererApp.Map.get_map_state(map_id)
|
||||
|
||||
%{"x" => x, "y" => y} =
|
||||
coordinates
|
||||
|> case do
|
||||
%{"x" => x, "y" => y} ->
|
||||
%{"x" => x, "y" => y}
|
||||
%{"x" => x, "y" => y} =
|
||||
coordinates
|
||||
|> case do
|
||||
%{"x" => x, "y" => y} ->
|
||||
%{"x" => x, "y" => y}
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
|
||||
system_result =
|
||||
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
|
||||
{:ok, existing_system} when not is_nil(existing_system) ->
|
||||
use_old_coordinates = Map.get(system_info, :use_old_coordinates, false)
|
||||
{:ok, system} =
|
||||
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
|
||||
{:ok, existing_system} when not is_nil(existing_system) ->
|
||||
use_old_coordinates = Map.get(system_info, :use_old_coordinates, false)
|
||||
|
||||
if use_old_coordinates do
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: existing_system.position_x,
|
||||
position_y: existing_system.position_y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
if use_old_coordinates do
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: existing_system.position_x,
|
||||
position_y: existing_system.position_y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
|
||||
existing_system
|
||||
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
|
||||
else
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
existing_system
|
||||
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
|
||||
else
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
|
||||
existing_system
|
||||
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|
||||
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|
||||
|> WandererApp.MapSystemRepo.cleanup_tags!()
|
||||
|> WandererApp.MapSystemRepo.cleanup_temporary_name!()
|
||||
|> WandererApp.MapSystemRepo.cleanup_linked_sig_eve_id!()
|
||||
|> maybe_update_extra_info(extra_info)
|
||||
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
|
||||
end
|
||||
existing_system
|
||||
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|
||||
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|
||||
|> WandererApp.MapSystemRepo.cleanup_tags!()
|
||||
|> WandererApp.MapSystemRepo.cleanup_temporary_name!()
|
||||
|> WandererApp.MapSystemRepo.cleanup_linked_sig_eve_id!()
|
||||
|> maybe_update_extra_info(extra_info)
|
||||
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
|
||||
end
|
||||
|
||||
_ ->
|
||||
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, solar_system_info} ->
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
_ ->
|
||||
{:ok, solar_system_info} =
|
||||
WandererApp.CachedInfo.get_system_static_info(solar_system_id)
|
||||
|
||||
WandererApp.MapSystemRepo.create(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info for #{solar_system_id}: #{inspect(reason)}")
|
||||
{:error, :system_info_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
case system_result do
|
||||
{:ok, system} ->
|
||||
:ok = WandererApp.Map.add_system(map_id, system)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:system_#{system.id}:last_activity",
|
||||
DateTime.utc_now(),
|
||||
ttl: @system_inactive_timeout
|
||||
@ddrt.insert(
|
||||
{solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(%{
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})},
|
||||
rtree_name
|
||||
)
|
||||
|
||||
Impl.broadcast!(map_id, :add_system, system)
|
||||
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
Logger.debug(fn ->
|
||||
"SystemsImpl.do_add_system calling ExternalEvents.broadcast for map #{map_id}, system: #{solar_system_id}"
|
||||
end)
|
||||
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :add_system, %{
|
||||
solar_system_id: system.solar_system_id,
|
||||
position_x: system.position_x,
|
||||
position_y: system.position_y
|
||||
WandererApp.MapSystemRepo.create(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: x,
|
||||
position_y: y
|
||||
})
|
||||
|
||||
track_add_system(map_id, user_id, character_id, system.solar_system_id)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.error("Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}")
|
||||
error
|
||||
end
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn ->
|
||||
"Cannot add system #{solar_system_id} to map #{map_id}: map does not exist in database"
|
||||
end)
|
||||
|
||||
{:error, :map_not_found}
|
||||
:ok = WandererApp.Map.add_system(map_id, system)
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to verify map #{map_id} exists: #{inspect(error)}")
|
||||
{:error, :map_verification_failed}
|
||||
end
|
||||
end
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:system_#{system.id}:last_activity",
|
||||
DateTime.utc_now(),
|
||||
ttl: @system_inactive_timeout
|
||||
)
|
||||
|
||||
Impl.broadcast!(map_id, :add_system, system)
|
||||
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
Logger.debug(fn ->
|
||||
"SystemsImpl.do_add_system calling ExternalEvents.broadcast for map #{map_id}, system: #{solar_system_id}"
|
||||
end)
|
||||
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :add_system, %{
|
||||
solar_system_id: system.solar_system_id,
|
||||
name: system.name,
|
||||
position_x: system.position_x,
|
||||
position_y: system.position_y
|
||||
})
|
||||
|
||||
defp track_add_system(map_id, user_id, character_id, solar_system_id) do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:system_added, %{
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
@@ -969,7 +930,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
Impl.broadcast!(map_id, :update_system, updated_system)
|
||||
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
# This may fail if the relay is not available (e.g., in tests), which is fine
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :system_metadata_changed, %{
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
name: updated_system.name,
|
||||
@@ -978,7 +938,5 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
description: updated_system.description,
|
||||
status: updated_system.status
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,14 +132,9 @@ defmodule WandererApp.Maps do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
{:ok, acls} =
|
||||
WandererApp.Api.MapAccessList.read_by_map(%{map_id: map_id},
|
||||
load: [access_list: [:owner, :members]]
|
||||
)
|
||||
|
||||
map_acls =
|
||||
acls
|
||||
|> Enum.map(fn acl -> acl.access_list end)
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
@@ -203,7 +198,10 @@ defmodule WandererApp.Maps do
|
||||
is_member_corp = to_string(c.corporation_id) in map_member_corporation_ids
|
||||
is_member_alliance = to_string(c.alliance_id) in map_member_alliance_ids
|
||||
|
||||
is_owner || is_acl_owner || is_member_eve || is_member_corp || is_member_alliance
|
||||
has_access =
|
||||
is_owner or is_acl_owner or is_member_eve or is_member_corp or is_member_alliance
|
||||
|
||||
has_access
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -247,11 +245,11 @@ defmodule WandererApp.Maps do
|
||||
members ->
|
||||
members
|
||||
|> Enum.any?(fn member ->
|
||||
(member.role == :blocked &&
|
||||
(member.role == :blocked and
|
||||
member.eve_character_id in user_character_eve_ids) or
|
||||
(member.role == :blocked &&
|
||||
(member.role == :blocked and
|
||||
member.eve_corporation_id in user_character_corporation_ids) or
|
||||
(member.role == :blocked &&
|
||||
(member.role == :blocked and
|
||||
member.eve_alliance_id in user_character_alliance_ids)
|
||||
end)
|
||||
end
|
||||
@@ -334,7 +332,9 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
|
||||
def check_user_can_delete_map(map_slug, current_user) do
|
||||
WandererApp.MapRepo.get_by_slug_with_permissions(map_slug, current_user)
|
||||
map_slug
|
||||
|> WandererApp.Api.Map.get_map_by_slug()
|
||||
|> Ash.load([:owner, :acls, :user_permissions], actor: current_user)
|
||||
|> case do
|
||||
{:ok,
|
||||
%{
|
||||
|
||||
@@ -53,40 +53,22 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
def get_tracked_by_map_all(map_id),
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def track(%{map_id: map_id, character_id: character_id}) do
|
||||
def track(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
"Failed to track character: #{character_id} on map: #{map_id}, #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
})
|
||||
end
|
||||
|
||||
def untrack(%{map_id: map_id, character_id: character_id}) do
|
||||
def untrack(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
"Failed to untrack character: #{character_id} on map: #{map_id}, #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
})
|
||||
end
|
||||
|
||||
def track!(settings) do
|
||||
|
||||
@@ -97,17 +97,9 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
|> WandererApp.Api.MapConnection.update_custom_info(update)
|
||||
|
||||
def get_by_id(map_id, id) do
|
||||
# Use read_by_map action which doesn't have the FilterConnectionsByActorMap preparation
|
||||
# that was causing "filter being false" errors in tests
|
||||
import Ash.Query
|
||||
|
||||
WandererApp.Api.MapConnection
|
||||
|> Ash.Query.for_read(:read_by_map, %{map_id: map_id})
|
||||
|> Ash.Query.filter(id == ^id)
|
||||
|> Ash.read_one()
|
||||
|> case do
|
||||
{:ok, nil} -> {:error, :not_found}
|
||||
{:ok, conn} -> {:ok, conn}
|
||||
case WandererApp.Api.MapConnection.by_id(id) do
|
||||
{:ok, conn} when conn.map_id == map_id -> {:ok, conn}
|
||||
{:ok, _} -> {:error, :not_found}
|
||||
{:error, _} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
defmodule WandererApp.Repositories.MapContextHelper do
|
||||
@moduledoc """
|
||||
Helper for providing map context to Ash actions from internal callers.
|
||||
|
||||
When InjectMapFromActor is used, internal callers (map duplication, seeds, etc.)
|
||||
need a way to provide map context without going through token auth.
|
||||
This helper creates a minimal map struct for the context.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Build Ash context options from attributes containing map_id.
|
||||
|
||||
Returns a keyword list suitable for passing to Ash actions.
|
||||
If attrs contains :map_id, creates a context with a minimal map struct.
|
||||
If no map_id present, returns an empty list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> MapContextHelper.build_context(%{map_id: "123", name: "System"})
|
||||
[context: %{map: %{id: "123"}}]
|
||||
|
||||
iex> MapContextHelper.build_context(%{name: "System"})
|
||||
[]
|
||||
|
||||
iex> MapContextHelper.build_context(%{map_id: nil, name: "System"})
|
||||
[]
|
||||
"""
|
||||
def build_context(attrs) when is_map(attrs) do
|
||||
case Map.get(attrs, :map_id) do
|
||||
nil -> []
|
||||
map_id -> [context: %{map: %{id: map_id}}]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Wraps an Ash action call with map context.
|
||||
|
||||
Deprecated: Use `build_context/1` instead for a simpler API.
|
||||
|
||||
## Examples
|
||||
|
||||
# Deprecated callback-based approach
|
||||
MapContextHelper.with_map_context(%{map_id: "123", name: "System"}, fn attrs, context ->
|
||||
WandererApp.Api.MapSystem.create(attrs, context)
|
||||
end)
|
||||
|
||||
# Preferred approach using build_context/1
|
||||
context = MapContextHelper.build_context(attrs)
|
||||
WandererApp.Api.MapSystem.create(attrs, context)
|
||||
"""
|
||||
@deprecated "Use build_context/1 instead"
|
||||
def with_map_context(attrs, fun) when is_map(attrs) and is_function(fun, 2) do
|
||||
context = build_context(attrs)
|
||||
fun.(attrs, context)
|
||||
end
|
||||
end
|
||||
@@ -26,20 +26,11 @@ defmodule WandererApp.MapRepo do
|
||||
end
|
||||
end
|
||||
|
||||
def get_by_slug_with_permissions(map_slug, current_user) do
|
||||
map_slug
|
||||
|> WandererApp.Api.Map.get_map_by_slug!()
|
||||
|> Ash.load(
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|> case do
|
||||
{:ok, map_with_acls} -> Ash.load(map_with_acls, :user_permissions, actor: current_user)
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
def get_by_slug_with_permissions(map_slug, current_user),
|
||||
do:
|
||||
map_slug
|
||||
|> WandererApp.Api.Map.get_map_by_slug()
|
||||
|> load_user_permissions(current_user)
|
||||
|
||||
@doc """
|
||||
Safely retrieves a map by slug, handling the case where multiple maps
|
||||
@@ -69,19 +60,13 @@ defmodule WandererApp.MapRepo do
|
||||
handle_multiple_results(slug, multiple_results_error, retry_count)
|
||||
|
||||
:error ->
|
||||
# Check if this is a no results error
|
||||
if is_no_results_error?(error) do
|
||||
Logger.debug("Map not found with slug: #{slug}")
|
||||
{:error, :not_found}
|
||||
else
|
||||
# Some other Invalid error
|
||||
Logger.error("Error retrieving map by slug",
|
||||
slug: slug,
|
||||
error: inspect(error)
|
||||
)
|
||||
# Some other Invalid error
|
||||
Logger.error("Error retrieving map by slug",
|
||||
slug: slug,
|
||||
error: inspect(error)
|
||||
)
|
||||
|
||||
{:error, :unknown_error}
|
||||
end
|
||||
{:error, :unknown_error}
|
||||
end
|
||||
|
||||
error in Ash.Error.Query.NotFound ->
|
||||
@@ -157,18 +142,17 @@ defmodule WandererApp.MapRepo do
|
||||
end)
|
||||
end
|
||||
|
||||
# Helper function to check if an error indicates no results were found
|
||||
defp is_no_results_error?(%Ash.Error.Invalid{errors: errors}) do
|
||||
# If errors list is empty, it's likely a no results error
|
||||
Enum.empty?(errors)
|
||||
end
|
||||
|
||||
defp is_no_results_error?(_), do: false
|
||||
|
||||
def load_relationships(map, []), do: {:ok, map}
|
||||
|
||||
def load_relationships(map, relationships), do: map |> Ash.load(relationships)
|
||||
|
||||
defp load_user_permissions({:ok, map}, current_user),
|
||||
do:
|
||||
map
|
||||
|> Ash.load([:acls, :user_permissions], actor: current_user)
|
||||
|
||||
defp load_user_permissions(error, _current_user), do: error
|
||||
|
||||
def update_hubs(map_id, hubs) do
|
||||
map_id
|
||||
|> WandererApp.Api.Map.by_id()
|
||||
|
||||
@@ -4,10 +4,10 @@ defmodule WandererApp.MapSystemCommentRepo do
|
||||
require Logger
|
||||
|
||||
def get_by_id(comment_id),
|
||||
do: WandererApp.Api.MapSystemComment.by_id(comment_id)
|
||||
do: WandererApp.Api.MapSystemComment.by_id!(comment_id) |> Ash.load([:system])
|
||||
|
||||
def get_by_system(system_id),
|
||||
do: WandererApp.Api.MapSystemComment.by_system_id(system_id, load: [:character])
|
||||
do: WandererApp.Api.MapSystemComment.by_system_id(system_id)
|
||||
|
||||
def create(comment), do: comment |> WandererApp.Api.MapSystemComment.create()
|
||||
def create!(comment), do: comment |> WandererApp.Api.MapSystemComment.create!()
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
defmodule WandererApp.MapSystemRepo do
|
||||
use WandererApp, :repository
|
||||
|
||||
alias WandererApp.Repositories.MapContextHelper
|
||||
|
||||
def create(system) do
|
||||
context = MapContextHelper.build_context(system)
|
||||
WandererApp.Api.MapSystem.create(system, context)
|
||||
system |> WandererApp.Api.MapSystem.create()
|
||||
end
|
||||
|
||||
def upsert(system) do
|
||||
@@ -13,15 +10,12 @@ defmodule WandererApp.MapSystemRepo do
|
||||
end
|
||||
|
||||
def get_by_map_and_solar_system_id(map_id, solar_system_id) do
|
||||
WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
WandererApp.Api.MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id)
|
||||
|> case do
|
||||
{:ok, system} ->
|
||||
{:ok, system}
|
||||
|
||||
_error ->
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
@@ -129,16 +123,10 @@ defmodule WandererApp.MapSystemRepo do
|
||||
system
|
||||
|> WandererApp.Api.MapSystem.update_description(update)
|
||||
|
||||
def update_locked(system, update) do
|
||||
case WandererApp.Api.MapSystem.update_locked(system, update) do
|
||||
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Changes.StaleRecord{}]}} ->
|
||||
WandererApp.Api.MapSystem.by_id!(system.id)
|
||||
|> WandererApp.Api.MapSystem.update_locked(update)
|
||||
|
||||
{:ok, system} ->
|
||||
{:ok, system}
|
||||
end
|
||||
end
|
||||
def update_locked(system, update),
|
||||
do:
|
||||
system
|
||||
|> WandererApp.Api.MapSystem.update_locked(update)
|
||||
|
||||
def update_status(system, update),
|
||||
do:
|
||||
@@ -155,11 +143,6 @@ defmodule WandererApp.MapSystemRepo do
|
||||
|> WandererApp.Api.MapSystem.update_temporary_name(update)
|
||||
end
|
||||
|
||||
def update_custom_name(system, update) do
|
||||
system
|
||||
|> WandererApp.Api.MapSystem.update_custom_name(update)
|
||||
end
|
||||
|
||||
def update_labels(system, update),
|
||||
do:
|
||||
system
|
||||
|
||||
@@ -501,16 +501,13 @@ defmodule WandererApp.SecurityAudit do
|
||||
# Ensure event_type is properly formatted
|
||||
event_type = normalize_event_type(audit_entry.event_type)
|
||||
|
||||
# Generate unique entity_id to avoid constraint violations
|
||||
entity_id = generate_entity_id(audit_entry.session_id)
|
||||
|
||||
attrs = %{
|
||||
entity_id: entity_id,
|
||||
user_id: audit_entry.user_id,
|
||||
character_id: nil,
|
||||
entity_id: hash_identifier(audit_entry.session_id),
|
||||
entity_type: :security_event,
|
||||
event_type: event_type,
|
||||
event_data: encode_event_data(audit_entry),
|
||||
user_id: audit_entry.user_id,
|
||||
character_id: nil
|
||||
event_data: encode_event_data(audit_entry)
|
||||
}
|
||||
|
||||
case UserActivity.new(attrs) do
|
||||
@@ -622,13 +619,8 @@ defmodule WandererApp.SecurityAudit do
|
||||
defp convert_datetime(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
|
||||
defp convert_datetime(value), do: value
|
||||
|
||||
defp generate_entity_id(session_id \\ nil) do
|
||||
if session_id do
|
||||
# Include high-resolution timestamp and unique component for guaranteed uniqueness
|
||||
"#{hash_identifier(session_id)}_#{:os.system_time(:microsecond)}_#{System.unique_integer([:positive])}"
|
||||
else
|
||||
"audit_#{:os.system_time(:microsecond)}_#{System.unique_integer([:positive])}"
|
||||
end
|
||||
defp generate_entity_id do
|
||||
"audit_#{DateTime.utc_now() |> DateTime.to_unix(:microsecond)}_#{System.unique_integer([:positive])}"
|
||||
end
|
||||
|
||||
defp async_enabled? do
|
||||
|
||||
@@ -88,21 +88,20 @@ defmodule WandererApp.SecurityAudit.AsyncProcessor do
|
||||
def handle_cast({:log_event, audit_entry}, state) do
|
||||
# Add to buffer
|
||||
buffer = [audit_entry | state.buffer]
|
||||
buf_len = length(buffer)
|
||||
|
||||
# Update stats
|
||||
stats = Map.update!(state.stats, :events_processed, &(&1 + 1))
|
||||
|
||||
# Check if we need to flush
|
||||
cond do
|
||||
buf_len >= state.batch_size ->
|
||||
length(buffer) >= state.batch_size ->
|
||||
# Flush immediately if batch size reached
|
||||
{:noreply, do_flush(%{state | buffer: buffer, stats: stats})}
|
||||
|
||||
buf_len >= @max_buffer_size ->
|
||||
length(buffer) >= @max_buffer_size ->
|
||||
# Force flush if max buffer size reached
|
||||
Logger.warning("Security audit buffer overflow, forcing flush",
|
||||
buffer_size: buf_len,
|
||||
buffer_size: length(buffer),
|
||||
max_size: @max_buffer_size
|
||||
)
|
||||
|
||||
@@ -187,66 +186,23 @@ defmodule WandererApp.SecurityAudit.AsyncProcessor do
|
||||
# Clear buffer
|
||||
%{state | buffer: [], stats: stats}
|
||||
|
||||
{:partial, success_count, failed_events} ->
|
||||
failed_count = length(failed_events)
|
||||
|
||||
Logger.warning(
|
||||
"Partial flush: stored #{success_count}, failed #{failed_count} audit events",
|
||||
success_count: success_count,
|
||||
failed_count: failed_count,
|
||||
buffer_size: length(state.buffer)
|
||||
)
|
||||
|
||||
# Emit telemetry for monitoring
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit, :async_flush_partial],
|
||||
%{success_count: success_count, failed_count: failed_count},
|
||||
%{}
|
||||
)
|
||||
|
||||
# Update stats - count partial flush as both success and error
|
||||
stats =
|
||||
state.stats
|
||||
|> Map.update!(:batches_flushed, &(&1 + 1))
|
||||
|> Map.update!(:errors, &(&1 + 1))
|
||||
|> Map.put(:last_flush, DateTime.utc_now())
|
||||
|
||||
# Extract just the events from failed_events tuples
|
||||
failed_only = Enum.map(failed_events, fn {event, _reason} -> event end)
|
||||
|
||||
remaining_buffer = Enum.reject(state.buffer, fn ev -> ev in failed_only end)
|
||||
|
||||
# Re-buffer failed events at the front, preserving newest-first ordering
|
||||
# Reverse failed_only since flush reversed the buffer to oldest-first
|
||||
new_buffer = Enum.reverse(failed_only) ++ remaining_buffer
|
||||
buffer = handle_buffer_overflow(new_buffer, @max_buffer_size)
|
||||
|
||||
%{state | buffer: buffer, stats: stats}
|
||||
|
||||
{:error, failed_events} ->
|
||||
failed_count = length(failed_events)
|
||||
|
||||
Logger.error("Failed to flush all #{failed_count} security audit events",
|
||||
failed_count: failed_count,
|
||||
buffer_size: length(state.buffer)
|
||||
)
|
||||
|
||||
# Emit telemetry for monitoring
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit, :async_flush_failure],
|
||||
%{count: 1, event_count: failed_count},
|
||||
%{}
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to flush security audit events",
|
||||
reason: inspect(reason),
|
||||
event_count: length(events)
|
||||
)
|
||||
|
||||
# Update error stats
|
||||
stats = Map.update!(state.stats, :errors, &(&1 + 1))
|
||||
|
||||
# Extract just the events from failed_events tuples
|
||||
failed_only = Enum.map(failed_events, fn {event, _reason} -> event end)
|
||||
|
||||
# Since ALL events failed, the new buffer should only contain the failed events
|
||||
# Reverse to maintain newest-first ordering (flush reversed to oldest-first)
|
||||
buffer = handle_buffer_overflow(Enum.reverse(failed_only), @max_buffer_size)
|
||||
# Implement backoff - keep events in buffer but don't grow indefinitely
|
||||
buffer =
|
||||
if length(state.buffer) > @max_buffer_size do
|
||||
Logger.warning("Dropping oldest audit events due to repeated flush failures")
|
||||
Enum.take(state.buffer, @max_buffer_size)
|
||||
else
|
||||
state.buffer
|
||||
end
|
||||
|
||||
%{state | buffer: buffer, stats: stats}
|
||||
end
|
||||
@@ -257,100 +213,34 @@ defmodule WandererApp.SecurityAudit.AsyncProcessor do
|
||||
events
|
||||
# Ash bulk operations work better with smaller chunks
|
||||
|> Enum.chunk_every(50)
|
||||
|> Enum.reduce({0, []}, fn chunk, {total_success, all_failed} ->
|
||||
|> Enum.reduce_while({:ok, 0}, fn chunk, {:ok, count} ->
|
||||
case store_event_chunk(chunk) do
|
||||
{:ok, chunk_count} ->
|
||||
{total_success + chunk_count, all_failed}
|
||||
{:cont, {:ok, count + chunk_count}}
|
||||
|
||||
{:partial, chunk_count, failed_events} ->
|
||||
{total_success + chunk_count, all_failed ++ failed_events}
|
||||
|
||||
{:error, failed_events} ->
|
||||
{total_success, all_failed ++ failed_events}
|
||||
end
|
||||
end)
|
||||
|> then(fn {success_count, failed_events_list} ->
|
||||
# Derive the final return shape based on results
|
||||
cond do
|
||||
failed_events_list == [] ->
|
||||
{:ok, success_count}
|
||||
|
||||
success_count == 0 ->
|
||||
{:error, failed_events_list}
|
||||
|
||||
true ->
|
||||
{:partial, success_count, failed_events_list}
|
||||
{:error, _} = error ->
|
||||
{:halt, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_buffer_overflow(buffer, max_size) when length(buffer) > max_size do
|
||||
dropped = length(buffer) - max_size
|
||||
|
||||
Logger.warning(
|
||||
"Dropping #{dropped} oldest audit events due to buffer overflow",
|
||||
buffer_size: length(buffer),
|
||||
max_size: max_size
|
||||
)
|
||||
|
||||
# Emit telemetry for dropped events
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit, :events_dropped],
|
||||
%{count: dropped},
|
||||
%{}
|
||||
)
|
||||
|
||||
# Keep the newest events (take from the front since buffer is newest-first)
|
||||
Enum.take(buffer, max_size)
|
||||
end
|
||||
|
||||
defp handle_buffer_overflow(buffer, _max_size), do: buffer
|
||||
|
||||
defp store_event_chunk(events) do
|
||||
# Process each event and partition results
|
||||
{successes, failures} =
|
||||
events
|
||||
|> Enum.map(fn event ->
|
||||
case SecurityAudit.do_store_audit_entry(event) do
|
||||
:ok ->
|
||||
{:ok, event}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to store individual audit event",
|
||||
error: inspect(reason),
|
||||
event_type: Map.get(event, :event_type),
|
||||
user_id: Map.get(event, :user_id)
|
||||
)
|
||||
|
||||
{:error, {event, reason}}
|
||||
end
|
||||
end)
|
||||
|> Enum.split_with(fn
|
||||
{:ok, _} -> true
|
||||
{:error, _} -> false
|
||||
# Transform events to Ash attributes
|
||||
records =
|
||||
Enum.map(events, fn event ->
|
||||
SecurityAudit.do_store_audit_entry(event)
|
||||
end)
|
||||
|
||||
successful_count = length(successes)
|
||||
failed_count = length(failures)
|
||||
# Count successful stores
|
||||
successful =
|
||||
Enum.count(records, fn
|
||||
:ok -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
# Extract failed events with reasons
|
||||
failed_events = Enum.map(failures, fn {:error, event_reason} -> event_reason end)
|
||||
|
||||
# Log if some events failed (telemetry will be emitted at flush level)
|
||||
if failed_count > 0 do
|
||||
Logger.debug("Chunk processing: #{failed_count} of #{length(events)} events failed")
|
||||
end
|
||||
|
||||
# Return richer result shape
|
||||
cond do
|
||||
successful_count == 0 ->
|
||||
{:error, failed_events}
|
||||
|
||||
failed_count > 0 ->
|
||||
{:partial, successful_count, failed_events}
|
||||
|
||||
true ->
|
||||
{:ok, successful_count}
|
||||
end
|
||||
{:ok, successful}
|
||||
rescue
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,16 +12,11 @@ defmodule WandererAppWeb.ApiSpecV1 do
|
||||
# Get the base spec from the original
|
||||
base_spec = WandererAppWeb.ApiSpec.spec()
|
||||
|
||||
# Get v1 spec
|
||||
# Get v1 spec
|
||||
v1_spec = WandererAppWeb.OpenApiV1Spec.spec()
|
||||
|
||||
# Tag legacy paths and v1 paths appropriately
|
||||
tagged_legacy_paths = tag_paths(base_spec.paths || %{}, "Legacy API")
|
||||
# v1 paths already have tags from AshJsonApi, keep them as-is
|
||||
v1_paths = v1_spec.paths || %{}
|
||||
|
||||
# Merge the specs
|
||||
merged_paths = Map.merge(tagged_legacy_paths, v1_paths)
|
||||
merged_paths = Map.merge(base_spec.paths || %{}, v1_spec.paths || %{})
|
||||
|
||||
# Merge components
|
||||
merged_components = %Components{
|
||||
@@ -89,53 +84,11 @@ defmodule WandererAppWeb.ApiSpecV1 do
|
||||
# Get tags from v1 spec if available
|
||||
spec_tags = Map.get(v1_spec, :tags, [])
|
||||
|
||||
base_tags ++ spec_tags
|
||||
# Add custom v1 tags
|
||||
v1_label_tags = [
|
||||
%{name: "v1 JSON:API", description: "JSON:API compliant endpoints with advanced querying"}
|
||||
]
|
||||
|
||||
base_tags ++ v1_label_tags ++ spec_tags
|
||||
end
|
||||
|
||||
# Tag all operations in paths with the given tag
|
||||
defp tag_paths(paths, tag) when is_map(paths) do
|
||||
Map.new(paths, fn {path, path_item} ->
|
||||
{path, tag_path_item(path_item, tag)}
|
||||
end)
|
||||
end
|
||||
|
||||
# Handle OpenApiSpex.PathItem structs
|
||||
defp tag_path_item(%OpenApiSpex.PathItem{} = path_item, tag) do
|
||||
path_item
|
||||
|> maybe_tag_operation(:get, tag)
|
||||
|> maybe_tag_operation(:put, tag)
|
||||
|> maybe_tag_operation(:post, tag)
|
||||
|> maybe_tag_operation(:delete, tag)
|
||||
|> maybe_tag_operation(:patch, tag)
|
||||
|> maybe_tag_operation(:options, tag)
|
||||
|> maybe_tag_operation(:head, tag)
|
||||
end
|
||||
|
||||
# Handle plain maps (from AshJsonApi)
|
||||
defp tag_path_item(path_item, tag) when is_map(path_item) do
|
||||
Map.new(path_item, fn {method, operation} ->
|
||||
{method, add_tag_to_operation(operation, tag)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp tag_path_item(path_item, _tag), do: path_item
|
||||
|
||||
defp maybe_tag_operation(path_item, method, tag) do
|
||||
case Map.get(path_item, method) do
|
||||
nil -> path_item
|
||||
operation -> Map.put(path_item, method, add_tag_to_operation(operation, tag))
|
||||
end
|
||||
end
|
||||
|
||||
defp add_tag_to_operation(%OpenApiSpex.Operation{} = operation, tag) do
|
||||
%{operation | tags: [tag | List.wrap(operation.tags)]}
|
||||
end
|
||||
|
||||
defp add_tag_to_operation(%{} = operation, tag) do
|
||||
Map.update(operation, :tags, [tag], fn existing_tags ->
|
||||
[tag | List.wrap(existing_tags)]
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_tag_to_operation(operation, _tag), do: operation
|
||||
end
|
||||
|
||||
@@ -6,12 +6,5 @@ defmodule WandererAppWeb.ApiV1Router do
|
||||
json_schema: "/json_schema",
|
||||
open_api_title: "WandererApp v1 JSON:API",
|
||||
open_api_version: "1.0.0",
|
||||
modify_open_api: {WandererAppWeb.OpenApi, :spec, []},
|
||||
modify_conn: {__MODULE__, :add_context, []}
|
||||
|
||||
def add_context(conn, _resource) do
|
||||
# Actor is set by CheckJsonApiAuth using Ash.PlugHelpers.set_actor/2
|
||||
# The actor (ActorWithMap) is passed to Ash actions automatically
|
||||
conn
|
||||
end
|
||||
modify_open_api: {WandererAppWeb.OpenApi, :spec, []}
|
||||
end
|
||||
|
||||
@@ -3,37 +3,6 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
Controller for Server-Sent Events (SSE) streaming.
|
||||
|
||||
Provides real-time event streaming for map updates to external clients.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All error responses use structured JSON format for consistency with the API:
|
||||
|
||||
{
|
||||
"error": "Human-readable error message",
|
||||
"code": "MACHINE_READABLE_CODE",
|
||||
"status": 403
|
||||
}
|
||||
|
||||
## Error Codes
|
||||
|
||||
- `SSE_GLOBALLY_DISABLED` - SSE disabled in server configuration
|
||||
- `SSE_DISABLED_FOR_MAP` - SSE disabled for this specific map
|
||||
- `SUBSCRIPTION_REQUIRED` - Active subscription required (Enterprise mode)
|
||||
- `MAP_NOT_FOUND` - Requested map does not exist
|
||||
- `UNAUTHORIZED` - Invalid or missing API key
|
||||
- `MAP_CONNECTION_LIMIT` - Too many concurrent connections to this map
|
||||
- `API_KEY_CONNECTION_LIMIT` - Too many connections for this API key
|
||||
- `INTERNAL_SERVER_ERROR` - Unexpected server error
|
||||
|
||||
## Access Control
|
||||
|
||||
SSE connections require:
|
||||
1. Valid API key (Bearer token)
|
||||
2. SSE enabled globally (server config)
|
||||
3. SSE enabled for the specific map
|
||||
4. Active subscription (Enterprise mode only)
|
||||
|
||||
See `WandererApp.ExternalEvents.SseAccessControl` for details.
|
||||
"""
|
||||
|
||||
use WandererAppWeb, :controller
|
||||
@@ -59,55 +28,25 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
- format: Event format - "legacy" (default) or "jsonapi" for JSON:API compliance
|
||||
"""
|
||||
def stream(conn, %{"map_identifier" => map_identifier} = params) do
|
||||
case validate_api_key(conn, map_identifier) do
|
||||
{:ok, map, api_key} ->
|
||||
case WandererApp.ExternalEvents.SseAccessControl.sse_allowed?(map.id) do
|
||||
:ok ->
|
||||
establish_sse_connection(conn, map.id, api_key, params)
|
||||
Logger.debug(fn -> "SSE stream requested for map #{map_identifier}" end)
|
||||
|
||||
{:error, :sse_globally_disabled} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
503,
|
||||
"Server-Sent Events are disabled on this server",
|
||||
"SSE_GLOBALLY_DISABLED"
|
||||
)
|
||||
# Check if SSE is enabled
|
||||
unless WandererApp.Env.sse_enabled?() do
|
||||
conn
|
||||
|> put_status(:service_unavailable)
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(503, "Server-Sent Events are disabled on this server")
|
||||
else
|
||||
# Validate API key and get map
|
||||
case validate_api_key(conn, map_identifier) do
|
||||
{:ok, map, api_key} ->
|
||||
establish_sse_connection(conn, map.id, api_key, params)
|
||||
|
||||
{:error, :sse_disabled_for_map} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
403,
|
||||
"Server-Sent Events are disabled for this map",
|
||||
"SSE_DISABLED_FOR_MAP"
|
||||
)
|
||||
|
||||
{:error, :subscription_required} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
402,
|
||||
"Active subscription required for Server-Sent Events",
|
||||
"SUBSCRIPTION_REQUIRED"
|
||||
)
|
||||
|
||||
{:error, _reason} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
403,
|
||||
"Server-Sent Events not available",
|
||||
"SSE_NOT_AVAILABLE"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, status, message} ->
|
||||
# Map validation errors to appropriate codes
|
||||
code =
|
||||
case status do
|
||||
401 -> "UNAUTHORIZED"
|
||||
404 -> "MAP_NOT_FOUND"
|
||||
_ -> "SSE_ERROR"
|
||||
end
|
||||
|
||||
send_sse_error(conn, status, message, code)
|
||||
{:error, status, message} ->
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{error: message})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,24 +105,27 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
stream_events(conn, map_id, api_key, event_filter, event_format)
|
||||
|
||||
{:error, :map_limit_exceeded} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
429,
|
||||
"Too many connections to this map",
|
||||
"MAP_CONNECTION_LIMIT"
|
||||
)
|
||||
conn
|
||||
|> put_status(:too_many_requests)
|
||||
|> json(%{
|
||||
error: "Too many connections to this map",
|
||||
code: "MAP_CONNECTION_LIMIT"
|
||||
})
|
||||
|
||||
{:error, :api_key_limit_exceeded} ->
|
||||
send_sse_error(
|
||||
conn,
|
||||
429,
|
||||
"Too many connections for this API key",
|
||||
"API_KEY_CONNECTION_LIMIT"
|
||||
)
|
||||
conn
|
||||
|> put_status(:too_many_requests)
|
||||
|> json(%{
|
||||
error: "Too many connections for this API key",
|
||||
code: "API_KEY_CONNECTION_LIMIT"
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to add SSE client: #{inspect(reason)}")
|
||||
send_sse_error(conn, 500, "Internal server error", "INTERNAL_SERVER_ERROR")
|
||||
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> send_resp(500, "Internal server error")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -347,19 +289,19 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
else
|
||||
[] ->
|
||||
Logger.warning("Missing or invalid 'Bearer' token")
|
||||
{:error, 401, "Missing or invalid 'Bearer' token"}
|
||||
{:error, :unauthorized, "Missing or invalid 'Bearer' token"}
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("Map not found: #{map_identifier}")
|
||||
{:error, 404, "Map not found"}
|
||||
{:error, :not_found, "Map not found"}
|
||||
|
||||
false ->
|
||||
Logger.warning("Unauthorized: invalid token for map #{map_identifier}")
|
||||
{:error, 401, "Unauthorized (invalid token for map)"}
|
||||
{:error, :unauthorized, "Unauthorized (invalid token for map)"}
|
||||
|
||||
error ->
|
||||
Logger.error("Unexpected error validating API key: #{inspect(error)}")
|
||||
{:error, 500, "Unexpected error"}
|
||||
{:error, :internal_server_error, "Unexpected error"}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -379,25 +321,6 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
end
|
||||
end
|
||||
|
||||
# Sends a structured JSON error response for SSE connection failures.
|
||||
#
|
||||
# Returns consistent JSON format matching the rest of the API:
|
||||
# - error: Human-readable error message
|
||||
# - code: Machine-readable error code for programmatic handling
|
||||
# - status: HTTP status code
|
||||
#
|
||||
# This maintains API consistency and makes it easier for clients to
|
||||
# handle errors programmatically.
|
||||
defp send_sse_error(conn, status, message, code) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(%{
|
||||
error: message,
|
||||
code: code,
|
||||
status: status
|
||||
})
|
||||
end
|
||||
|
||||
# SSE helper functions
|
||||
|
||||
defp send_headers(conn) do
|
||||
|
||||
@@ -1320,9 +1320,9 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
errors:
|
||||
Enum.map(error.errors, fn err ->
|
||||
%{
|
||||
field: Map.get(err, :field) || Map.get(err, :input),
|
||||
message: Map.get(err, :message, "Unknown error"),
|
||||
value: Map.get(err, :value)
|
||||
field: err.field,
|
||||
message: err.message,
|
||||
value: err.value
|
||||
}
|
||||
end)
|
||||
})
|
||||
|
||||
@@ -115,7 +115,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
{:ok, period} <- APIUtils.require_param(params, "period"),
|
||||
query <- WandererApp.Map.Audit.get_map_activity_query(map_id, period, "all"),
|
||||
{:ok, data} <-
|
||||
Ash.read(query, read_opts()) do
|
||||
Ash.read(query) do
|
||||
data = Enum.map(data, &map_audit_event_to_json/1)
|
||||
json(conn, %{data: data})
|
||||
else
|
||||
@@ -131,18 +131,6 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
# In test environment, disable concurrency to avoid Ecto Sandbox ownership issues
|
||||
# In production, allow concurrent loading for better performance
|
||||
defp read_opts do
|
||||
base_opts = [authorize?: false]
|
||||
|
||||
if Application.get_env(:wanderer_app, :sql_sandbox) do
|
||||
Keyword.put(base_opts, :max_concurrency, 0)
|
||||
else
|
||||
base_opts
|
||||
end
|
||||
end
|
||||
|
||||
defp map_audit_event_to_json(
|
||||
%{event_type: event_type, event_data: event_data, character: character} = event
|
||||
) do
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
require Logger
|
||||
|
||||
alias OpenApiSpex.Schema
|
||||
alias WandererApp.MapConnectionRepo
|
||||
alias WandererApp.Map, as: MapData
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.ResponseSchemas
|
||||
@@ -180,8 +180,9 @@ defmodule WandererAppWeb.MapConnectionAPIController do
|
||||
|
||||
def index(%{assigns: %{map_id: map_id}} = conn, params) do
|
||||
with {:ok, src_filter} <- parse_optional(params, "solar_system_source"),
|
||||
{:ok, tgt_filter} <- parse_optional(params, "solar_system_target"),
|
||||
{:ok, conns} <- MapConnectionRepo.get_by_map(map_id) do
|
||||
{:ok, tgt_filter} <- parse_optional(params, "solar_system_target") do
|
||||
conns = MapData.list_connections!(map_id)
|
||||
|
||||
conns =
|
||||
conns
|
||||
|> filter_by_source(src_filter)
|
||||
|
||||
@@ -44,11 +44,6 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
delete(conn, params)
|
||||
end
|
||||
|
||||
def delete_single(conn, params) do
|
||||
# Delegate to existing delete action for compatibility
|
||||
delete(conn, params)
|
||||
end
|
||||
|
||||
# -- JSON Schemas --
|
||||
@map_system_schema %Schema{
|
||||
type: :object,
|
||||
@@ -536,67 +531,18 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
)
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
# Support both solar_system_id (integer) and system.id (UUID)
|
||||
with {:ok, system_identifier} <- parse_system_identifier(id),
|
||||
with {:ok, solar_system_id} <- APIUtils.parse_int(id),
|
||||
{:ok, attrs} <- APIUtils.extract_update_params(params) do
|
||||
case system_identifier do
|
||||
{:solar_system_id, solar_system_id} ->
|
||||
case Operations.update_system(conn, solar_system_id, attrs) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
case Operations.update_system(conn, solar_system_id, attrs) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
|
||||
{:system_id, system_uuid} ->
|
||||
# Handle update by system UUID
|
||||
map_id = conn.assigns[:map_id]
|
||||
|
||||
case WandererApp.Api.MapSystem.by_id(system_uuid) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
case Operations.update_system(conn, system.solar_system_id, attrs) do
|
||||
{:ok, result} ->
|
||||
APIUtils.respond_data(conn, result)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
|
||||
{:ok, _system} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_system_identifier(id) when is_binary(id) do
|
||||
case Ecto.UUID.cast(id) do
|
||||
{:ok, uuid} ->
|
||||
{:ok, {:system_id, uuid}}
|
||||
|
||||
:error ->
|
||||
case APIUtils.parse_int(id) do
|
||||
{:ok, solar_system_id} ->
|
||||
{:ok, {:solar_system_id, solar_system_id}}
|
||||
|
||||
{:error, msg} ->
|
||||
{:error, msg}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_system_identifier(id) when is_integer(id) do
|
||||
{:ok, {:solar_system_id, id}}
|
||||
end
|
||||
|
||||
defp parse_system_identifier(_id) do
|
||||
{:error, "Invalid system identifier"}
|
||||
end
|
||||
|
||||
operation(:delete_batch,
|
||||
summary: "Batch Delete Systems and Connections",
|
||||
parameters: [
|
||||
@@ -670,22 +616,6 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
responses: ResponseSchemas.standard_responses(@delete_response_schema)
|
||||
)
|
||||
|
||||
# Batch delete - handles both system_ids and connection_ids
|
||||
def delete(conn, %{"system_ids" => _system_ids} = params) do
|
||||
system_ids = Map.get(params, "system_ids", [])
|
||||
connection_ids = Map.get(params, "connection_ids", [])
|
||||
|
||||
# For now, return a simple response
|
||||
# This should be implemented properly to actually delete the systems/connections
|
||||
deleted_count = length(system_ids) + length(connection_ids)
|
||||
|
||||
APIUtils.respond_data(conn, %{
|
||||
deleted_count: deleted_count,
|
||||
deleted_systems: length(system_ids),
|
||||
deleted_connections: length(connection_ids)
|
||||
})
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
with {:ok, sid} <- APIUtils.parse_int(id),
|
||||
{:ok, _} <- Operations.delete_system(conn, sid) do
|
||||
@@ -712,16 +642,6 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
end
|
||||
end
|
||||
|
||||
# Catch-all clause for delete with missing or invalid parameters
|
||||
def delete(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> APIUtils.respond_data(%{
|
||||
deleted_count: 0,
|
||||
error: "Missing required parameters: system_ids or id"
|
||||
})
|
||||
end
|
||||
|
||||
# -- Legacy endpoints --
|
||||
|
||||
operation(:list_systems,
|
||||
|
||||
@@ -13,19 +13,10 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.User
|
||||
alias WandererApp.Api.ActorWithMap
|
||||
alias WandererApp.SecurityAudit
|
||||
alias WandererApp.Audit.RequestContext
|
||||
alias Ash.PlugHelpers
|
||||
|
||||
# Error messages for different failure reasons
|
||||
@error_messages %{
|
||||
map_owner_not_found: "Authentication failed",
|
||||
invalid_token: "Authentication failed",
|
||||
missing_auth_header: "Missing or invalid authorization header",
|
||||
invalid_session: "Invalid session"
|
||||
}
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
@@ -48,13 +39,9 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
%{auth_type: get_auth_type(conn), result: "success"}
|
||||
)
|
||||
|
||||
# Wrap user and map together as actor for Ash
|
||||
actor = ActorWithMap.new(user, map)
|
||||
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
|> assign(:current_user_role, get_user_role(user))
|
||||
|> PlugHelpers.set_actor(actor)
|
||||
|> maybe_assign_map(map)
|
||||
|
||||
{:ok, user} ->
|
||||
@@ -73,23 +60,16 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
%{auth_type: get_auth_type(conn), result: "success"}
|
||||
)
|
||||
|
||||
# Wrap user with nil map as actor for Ash (session auth has no map context)
|
||||
actor = ActorWithMap.new(user, nil)
|
||||
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
|> assign(:current_user_role, get_user_role(user))
|
||||
|> PlugHelpers.set_actor(actor)
|
||||
|
||||
{:error, reason} when is_atom(reason) ->
|
||||
# Error handling with atom reasons
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
# Legacy error handling for simple string errors
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
|
||||
# Get user-facing message from error messages map
|
||||
message = Map.get(@error_messages, reason, "Authentication failed")
|
||||
|
||||
# Log failed authentication with detailed internal reason
|
||||
# Log failed authentication
|
||||
request_details = extract_request_details(conn)
|
||||
|
||||
SecurityAudit.log_auth_event(
|
||||
@@ -108,7 +88,37 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: message}))
|
||||
|> send_resp(401, Jason.encode!(%{error: reason}))
|
||||
|> halt()
|
||||
|
||||
{:error, external_message, internal_reason} ->
|
||||
# New error handling with separate internal and external messages
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
|
||||
# Log failed authentication with detailed internal reason
|
||||
request_details = extract_request_details(conn)
|
||||
|
||||
SecurityAudit.log_auth_event(
|
||||
:auth_failure,
|
||||
nil,
|
||||
Map.merge(request_details, %{
|
||||
failure_reason: internal_reason,
|
||||
external_message: external_message
|
||||
})
|
||||
)
|
||||
|
||||
# Emit failed authentication event
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :json_api, :auth],
|
||||
%{count: 1, duration: duration},
|
||||
%{auth_type: get_auth_type(conn), result: "failure"}
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(401, Jason.encode!(%{error: external_message}))
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
@@ -123,7 +133,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
user_id ->
|
||||
case User.by_id(user_id, load: :characters) do
|
||||
{:ok, user} -> {:ok, user}
|
||||
{:error, _} -> {:error, :invalid_session}
|
||||
{:error, _} -> {:error, "Invalid session"}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -134,25 +144,89 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
validate_api_token(conn, token)
|
||||
|
||||
_ ->
|
||||
{:error, :missing_auth_header}
|
||||
{:error, "Missing or invalid authorization header"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_token(_conn, token) do
|
||||
# Token determines map - no need to check request params
|
||||
find_map_by_token(token)
|
||||
defp validate_api_token(conn, token) do
|
||||
# Try to get map identifier from multiple sources
|
||||
map_identifier = get_map_identifier(conn)
|
||||
|
||||
case map_identifier do
|
||||
nil ->
|
||||
# No map identifier found - this might be a general API endpoint
|
||||
{:error, "Authentication failed", :no_map_context}
|
||||
|
||||
identifier ->
|
||||
# Resolve the identifier (could be UUID or slug)
|
||||
case resolve_map_identifier(identifier) do
|
||||
{:ok, map} ->
|
||||
# Validate the token matches this specific map's API key
|
||||
if is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token) do
|
||||
# Get the map owner
|
||||
case User.by_id(map.owner.user_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
{:ok, user, map}
|
||||
|
||||
{:error, _error} ->
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
end
|
||||
else
|
||||
{:error, "Authentication failed", :invalid_token_for_map}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Authentication failed", :map_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_map_by_token(token) do
|
||||
case WandererApp.Api.Map.by_api_key(token, load: :owner) do
|
||||
{:ok, map} ->
|
||||
case User.by_id(map.owner.user_id, load: :characters) do
|
||||
{:ok, user} -> {:ok, user, map}
|
||||
_ -> {:error, :map_owner_not_found}
|
||||
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
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_token}
|
||||
# 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
|
||||
case WandererApp.Api.Map.by_id(identifier, load: :owner) do
|
||||
{:ok, map} ->
|
||||
{:ok, map}
|
||||
|
||||
_ ->
|
||||
# Try as slug
|
||||
WandererApp.Api.Map.get_map_by_slug(identifier, load: :owner)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckWebhooksDisabled do
|
||||
@moduledoc """
|
||||
Plug to check if webhooks are enabled.
|
||||
|
||||
This plug blocks access to webhook management endpoints when webhooks are disabled.
|
||||
Enable webhooks by setting WANDERER_WEBHOOKS_ENABLED=true in your environment.
|
||||
"""
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
if not WandererApp.Env.webhooks_enabled?() do
|
||||
conn
|
||||
|> send_resp(403, "Webhooks are disabled. Set WANDERER_WEBHOOKS_ENABLED=true to enable.")
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckWebsocketDisabled do
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
def call(conn, _opts) do
|
||||
if not WandererApp.Env.websocket_events_enabled?() do
|
||||
conn
|
||||
|> send_resp(403, "WebSocket events are disabled")
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -74,6 +74,8 @@ defmodule WandererAppWeb.Helpers.APIUtils do
|
||||
end
|
||||
|
||||
@spec parse_int(binary() | integer()) :: {:ok, integer()} | {:error, String.t()}
|
||||
def parse_int(nil), do: {:ok, nil}
|
||||
|
||||
def parse_int(str) when is_binary(str) do
|
||||
Logger.debug(fn -> "Parsing integer from: #{inspect(str)}" end)
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
options={Enum.map(@valid_types, fn valid_type -> {valid_type.label, valid_type.id} end)}
|
||||
/>
|
||||
|
||||
<!-- Modal action buttons -->
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="modal-action">
|
||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||
{(@live_action == :add_invite_link && "Add") || "Save"}
|
||||
|
||||
@@ -52,13 +52,10 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
solar_system_source_id_int = String.to_integer(solar_system_source_id)
|
||||
solar_system_target_id_int = String.to_integer(solar_system_target_id)
|
||||
|
||||
map_id
|
||||
|> WandererApp.Map.Server.add_connection(%{
|
||||
solar_system_source_id: solar_system_source_id_int,
|
||||
solar_system_target_id: solar_system_target_id_int,
|
||||
solar_system_source_id: solar_system_source_id |> String.to_integer(),
|
||||
solar_system_target_id: solar_system_target_id |> String.to_integer(),
|
||||
character_id: main_character_id
|
||||
})
|
||||
|
||||
@@ -66,8 +63,8 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
character_id: main_character_id,
|
||||
user_id: current_user_id,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id_int,
|
||||
solar_system_target_id: solar_system_target_id_int
|
||||
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
|
||||
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -203,15 +200,12 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
solar_system_source_id_int = String.to_integer(solar_system_source_id)
|
||||
solar_system_target_id_int = String.to_integer(solar_system_target_id)
|
||||
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_updated, %{
|
||||
character_id: main_character_id,
|
||||
user_id: current_user_id,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id_int,
|
||||
solar_system_target_id: solar_system_target_id_int,
|
||||
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
|
||||
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(),
|
||||
key: key_atom,
|
||||
value: value
|
||||
})
|
||||
@@ -219,8 +213,8 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
apply(WandererApp.Map.Server, method_atom, [
|
||||
map_id,
|
||||
%{
|
||||
solar_system_source_id: solar_system_source_id_int,
|
||||
solar_system_target_id: solar_system_target_id_int
|
||||
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
|
||||
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
|
||||
}
|
||||
|> Map.put_new(key_atom, value)
|
||||
])
|
||||
@@ -274,8 +268,8 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
defp get_connection_info(map_id, from, to) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.get_connection_info(%{
|
||||
solar_system_source_id: String.to_integer(from),
|
||||
solar_system_target_id: String.to_integer(to)
|
||||
solar_system_source_id: "#{from}" |> String.to_integer(),
|
||||
solar_system_target_id: "#{to}" |> String.to_integer()
|
||||
})
|
||||
|> case do
|
||||
{:ok, info} ->
|
||||
|
||||
@@ -22,9 +22,10 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
%{assigns: %{current_user: current_user, map_slug: map_slug}} = socket
|
||||
) do
|
||||
try do
|
||||
# Load acls with members first to avoid lateral join conflicts
|
||||
{:ok, %{id: map_id, user_permissions: user_permissions, owner_id: owner_id}} =
|
||||
WandererApp.MapRepo.get_by_slug_with_permissions(map_slug, current_user)
|
||||
map_slug
|
||||
|> WandererApp.Api.Map.get_map_by_slug!()
|
||||
|> Ash.load(:user_permissions, actor: current_user)
|
||||
|
||||
user_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("system_comment_added", %{
|
||||
solarSystemId: solar_system_id,
|
||||
comment: comment |> map_system_comment(solar_system_id)
|
||||
comment: comment |> map_system_comment()
|
||||
})
|
||||
|
||||
def handle_server_event(
|
||||
@@ -54,7 +54,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
when not is_nil(main_character_id) do
|
||||
system =
|
||||
WandererApp.Map.find_system_by_location(map_id, %{
|
||||
solar_system_id: solar_system_id
|
||||
solar_system_id: solar_system_id |> String.to_integer()
|
||||
})
|
||||
|
||||
comments_count =
|
||||
@@ -106,15 +106,18 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
socket
|
||||
) do
|
||||
WandererApp.Map.find_system_by_location(map_id, %{
|
||||
solar_system_id: solar_system_id
|
||||
solar_system_id: solar_system_id |> String.to_integer()
|
||||
})
|
||||
|> case do
|
||||
%{id: system_id} = system when not is_nil(system_id) ->
|
||||
%{id: system_id} when not is_nil(system_id) ->
|
||||
{:ok, comments} = WandererApp.MapSystemCommentRepo.get_by_system(system_id)
|
||||
|
||||
{:reply,
|
||||
%{comments: comments |> Enum.map(fn c -> map_system_comment(c, solar_system_id) end)},
|
||||
socket}
|
||||
comments =
|
||||
comments
|
||||
|> Enum.map(fn c -> c |> Ash.load!([:character, :system]) end)
|
||||
|> Enum.map(&map_system_comment/1)
|
||||
|
||||
{:reply, %{comments: comments}, socket}
|
||||
|
||||
_ ->
|
||||
{:reply, %{comments: []}, socket}
|
||||
@@ -168,24 +171,4 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
updated_at: updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def map_system_comment(nil, _solar_system_id), do: nil
|
||||
|
||||
def map_system_comment(
|
||||
%{
|
||||
id: id,
|
||||
character: character,
|
||||
text: text,
|
||||
updated_at: updated_at
|
||||
} = _comment,
|
||||
solar_system_id
|
||||
) do
|
||||
%{
|
||||
id: id,
|
||||
characterEveId: character.eve_id,
|
||||
solarSystemId: solar_system_id,
|
||||
text: text,
|
||||
updated_at: updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -306,13 +306,13 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
} = socket,
|
||||
type,
|
||||
body
|
||||
),
|
||||
do:
|
||||
socket
|
||||
|> Phoenix.LiveView.Utils.push_event("map_event", %{
|
||||
type: type,
|
||||
body: body
|
||||
})
|
||||
) do
|
||||
socket
|
||||
|> Phoenix.LiveView.Utils.push_event("map_event", %{
|
||||
type: type,
|
||||
body: body
|
||||
})
|
||||
end
|
||||
|
||||
def push_map_event(socket, _type, _body), do: socket
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
user_characters =
|
||||
active_characters
|
||||
|> Enum.map(&map_character/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
@@ -108,18 +107,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
WandererApp.Maps.check_user_can_delete_map(map_slug, current_user)
|
||||
|> case do
|
||||
{:ok, map} ->
|
||||
# Load the owner association to get character details
|
||||
map =
|
||||
case Ash.load(map, :owner) do
|
||||
{:ok, loaded_map} -> loaded_map |> map_map()
|
||||
_ -> map |> map_map()
|
||||
end
|
||||
|
||||
# Add owner to characters list, filtering out nil values
|
||||
characters =
|
||||
[map.owner |> map_character() | socket.assigns.characters]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.uniq()
|
||||
map = map |> map_map()
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :maps)
|
||||
@@ -127,7 +115,10 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:page_title, "Maps - Edit")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(:map_slug, map_slug)
|
||||
|> assign(:characters, characters)
|
||||
|> assign(
|
||||
:characters,
|
||||
[map.owner |> map_character() | socket.assigns.characters] |> Enum.uniq()
|
||||
)
|
||||
|> assign(
|
||||
:form,
|
||||
map |> AshPhoenix.Form.for_update(:update, forms: [auto?: true])
|
||||
@@ -163,7 +154,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:map_slug, map_slug)
|
||||
|> assign(:map_id, map.id)
|
||||
|> assign(:public_api_key, map.public_api_key)
|
||||
|> assign(:sse_enabled, map.sse_enabled)
|
||||
|> assign(:map, map)
|
||||
|> assign(
|
||||
export_settings: export_settings |> _get_export_map_data(),
|
||||
@@ -233,27 +223,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
{:noreply, assign(socket, public_api_key: new_api_key)}
|
||||
end
|
||||
|
||||
def handle_event("toggle-sse", _params, socket) do
|
||||
new_sse_enabled = not socket.assigns.sse_enabled
|
||||
map = socket.assigns.map
|
||||
|
||||
case WandererApp.Api.Map.toggle_sse(map, %{sse_enabled: new_sse_enabled}) do
|
||||
{:ok, updated_map} ->
|
||||
{:noreply, assign(socket, sse_enabled: new_sse_enabled, map: updated_map)}
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} ->
|
||||
error_message =
|
||||
errors
|
||||
|> Enum.map(fn error -> Map.get(error, :message, "Unknown error") end)
|
||||
|> Enum.join(", ")
|
||||
|
||||
{:noreply, put_flash(socket, :error, error_message)}
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to update SSE setting")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"live_select_change",
|
||||
|
||||
@@ -165,7 +165,6 @@
|
||||
field={f[:only_tracked_characters]}
|
||||
label="Allow only tracked characters"
|
||||
/>
|
||||
<.input type="checkbox" field={f[:sse_enabled]} label="Enable Server-Sent Events (SSE)" />
|
||||
<.input
|
||||
:if={@live_action == :create}
|
||||
type="checkbox"
|
||||
@@ -540,24 +539,6 @@
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-stone-700 mt-4 pt-4">
|
||||
<h3 class="text-md font-semibold mb-3">Server-Sent Events (SSE)</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={@sse_enabled}
|
||||
phx-click="toggle-sse"
|
||||
/>
|
||||
<span>Enable SSE for this map</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm text-stone-400 mt-2">
|
||||
When enabled, external clients can subscribe to real-time map events via SSE.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.live_component
|
||||
|
||||
@@ -201,8 +201,8 @@ defmodule WandererAppWeb.Router do
|
||||
plug WandererAppWeb.Plugs.CheckCharacterApiDisabled
|
||||
end
|
||||
|
||||
pipeline :api_webhooks do
|
||||
plug WandererAppWeb.Plugs.CheckWebhooksDisabled
|
||||
pipeline :api_websocket_events do
|
||||
plug WandererAppWeb.Plugs.CheckWebsocketDisabled
|
||||
end
|
||||
|
||||
pipeline :api_acl do
|
||||
@@ -302,9 +302,9 @@ defmodule WandererAppWeb.Router do
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
|
||||
# Webhook management endpoints (requires WANDERER_WEBHOOKS_ENABLED=true)
|
||||
# WebSocket events and webhook management endpoints (disabled by default)
|
||||
scope "/api/maps/:map_identifier", WandererAppWeb do
|
||||
pipe_through [:api, :api_map, :api_webhooks]
|
||||
pipe_through [:api, :api_map, :api_websocket_events]
|
||||
|
||||
get "/events", MapEventsAPIController, :list_events
|
||||
|
||||
@@ -363,6 +363,24 @@ defmodule WandererAppWeb.Router do
|
||||
#
|
||||
scope "/api", WandererAppWeb do
|
||||
pipe_through [:api]
|
||||
|
||||
# Basic health check for load balancers (lightweight)
|
||||
get "/health", Api.HealthController, :health
|
||||
|
||||
# Detailed health status for monitoring systems
|
||||
get "/health/status", Api.HealthController, :status
|
||||
|
||||
# Readiness check for deployment validation
|
||||
get "/health/ready", Api.HealthController, :ready
|
||||
|
||||
# Liveness check for container orchestration
|
||||
get "/health/live", Api.HealthController, :live
|
||||
|
||||
# Metrics endpoint for monitoring systems
|
||||
get "/health/metrics", Api.HealthController, :metrics
|
||||
|
||||
# Deep health check for comprehensive diagnostics
|
||||
get "/health/deep", Api.HealthController, :deep
|
||||
end
|
||||
|
||||
# scope "/api/licenses", WandererAppWeb do
|
||||
|
||||
8
mix.exs
8
mix.exs
@@ -97,12 +97,12 @@ defmodule WandererApp.MixProject do
|
||||
{:dart_sass, "~> 0.5.1", runtime: Mix.env() == :dev},
|
||||
{:oauth2, "~> 1.0 or ~> 2.0"},
|
||||
{:ueberauth, "~> 0.10.0"},
|
||||
{:req, "~> 0.5"},
|
||||
{:ash, "~> 3.9"},
|
||||
{:ash_cloak, "~> 0.1.7"},
|
||||
{:req, "~> 0.4.0"},
|
||||
{:ash, "~> 3.4"},
|
||||
{:ash_cloak, "~> 0.1.2"},
|
||||
{:ash_json_api, "~> 1.4"},
|
||||
{:ash_phoenix, "~> 2.1"},
|
||||
{:ash_postgres, "~> 2.6"},
|
||||
{:ash_postgres, "~> 2.4"},
|
||||
{:exsync, "~> 0.4", only: :dev},
|
||||
{:nimble_csv, "~> 1.2.0"},
|
||||
{:ecto_ulid_next, "~> 1.0.2"},
|
||||
|
||||
54
mix.lock
54
mix.lock
@@ -1,16 +1,16 @@
|
||||
%{
|
||||
"ash": {:hex, :ash, "3.9.0", "004371ffb181a142cda09544342dad1ffedf360a5636219d71cb5431ccbe3ad6", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e3e80a182c6e8b4d87f1caa4dba774da210f1f75f1420650ba012eb4388dc8c3"},
|
||||
"ash_cloak": {:hex, :ash_cloak, "0.1.7", "222137b911cf725189868ea266ec1ba8cebe6c14eed8df518dd3dd51cf8441d9", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "af3cb43a3fb16efe939aa3b4337bc1393bb6687cba5b731120590caae45f636b"},
|
||||
"ash": {:hex, :ash, "3.4.15", "0b8a0ae9bc543267380ffdacfeb1bc8d1bc831c1acb58b923ac0285464d5badd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.9", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3647184d23c40a8d4d381c3616b5c5c783d4d2e969918b6fd36aa171fede9cfa"},
|
||||
"ash_cloak": {:hex, :ash_cloak, "0.1.2", "d70338491ad8b6a18c691c25a2a236e18bb726c551642f56d996d25a9f1e779b", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "8b13dc44d8c58a7a876e537b3eab03672ac04f442568b4f9c1d70ccd9522812f"},
|
||||
"ash_json_api": {:hex, :ash_json_api, "1.4.10", "24e76a95ce0879c3dead994a9f727f7fc2de7678cdf7a265ba8fd0bbe939caa9", [:mix], [{:ash, "~> 3.3", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.34 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4", [hex: :json_xema, repo: "hexpm", optional: false]}, {:open_api_spex, "~> 3.16", [hex: :open_api_spex, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.10 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "8f38a6936725c9d1281f4f21e43d72474be7ed60f12ca47ff0f625a70dad52e7"},
|
||||
"ash_pagify": {:hex, :ash_pagify, "1.4.1", "af25d5f68b6df84ed5388dd4688658fd08fa59e99f70361a0497c376b50ac115", [:mix], [{:ash, "~> 3.3", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.1", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.1", [hex: :ash_postgres, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.37", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}], "hexpm", "5b7f771c5a76f92d120536cd87fb25b7321a681482aeaf127b7202bd18552c84"},
|
||||
"ash_phoenix": {:hex, :ash_phoenix, "2.1.2", "7215cf3a1ebc82ca0e5317a8449e1725fa753354674a0e8cd7fc1c8ffd1181c7", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "b591bd731a0855f670b5bc3f48c364b1694d508071f44d57bcd508c82817c51e"},
|
||||
"ash_postgres": {:hex, :ash_postgres, "2.6.25", "eb425722adf0c5d828c24810ed90aeef1fa1d8c0976579220e3739f102582176", [:mix], [{:ash, ">= 3.7.5 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.7 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "dadf95dfb33d2807cfd393b57315e703a9c938a8fffbdeb2a0f59f28f1909eca"},
|
||||
"ash_sql": {:hex, :ash_sql, "0.3.13", "1018528aa2589fc69240242c48f553b211899a574d860c564533c830644b5e25", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "2b65acafb677983d96aa6a9c43c107f71c63f3dd7bff220b55b99b3bdab53a95"},
|
||||
"ash_postgres": {:hex, :ash_postgres, "2.4.1", "6fa9bbb40e9d4a73bcdd2403e036874421e8c919dc57338eb6476cc8a82fa112", [:mix], [{:ash, ">= 3.4.9 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.30 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:inflex, "~> 2.1", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "9419993fe7f200db7230c372f5aa280f8bebb175501c9e8d58703c9054006c7b"},
|
||||
"ash_sql": {:hex, :ash_sql, "0.2.32", "de99255becfb9daa7991c18c870e9f276bb372acda7eda3e05c3e2ff2ca8922e", [:mix], [{:ash, ">= 3.1.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "43773bcd33d21319c11804d76fe11f1a1b7c8faba7aaedeab6f55fde3d2405db"},
|
||||
"bandit": {:hex, :bandit, "1.6.8", "be6fcbe01a74e6cba42ae35f4085acaeae9b2d8d360c0908d0b9addbc2811e47", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4fc08c8d4733735d175a007ecb25895e84d09292b0180a2e9f16948182c88b6e"},
|
||||
"better_number": {:hex, :better_number, "1.0.1", "5832757e2575feda6f6e67b3ff18f1510a42efec4f5673221f89cff8132add7b", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "782efdaf7bb4a7109265878fa30497a335bf7cd5954ce37ee539a3ce7cf09ceb"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
|
||||
"castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"},
|
||||
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
|
||||
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
|
||||
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
@@ -20,9 +20,8 @@
|
||||
"cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"},
|
||||
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
||||
"dart_sass": {:hex, :dart_sass, "0.5.1", "d45f20a8e324313689fb83287d4702352793ce8c9644bc254155d12656ade8b6", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "24f8a1c67e8b5267c51a33cbe6c0b5ebf12c2c83ace88b5ac04947d676b4ec81"},
|
||||
"db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"debounce_and_throttle": {:hex, :debounce_and_throttle, "0.9.0", "fa86c982963e00365cc9808afa496e82ca2b48f8905c6c79f8edd304800d0892", [:mix], [], "hexpm", "573a7cff4032754023d8e6874f3eff5354864c90b39b692f1fc4a44b3eb7517b"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
|
||||
@@ -31,8 +30,8 @@
|
||||
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
|
||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
|
||||
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
|
||||
"ecto_ulid_next": {:hex, :ecto_ulid_next, "1.0.2", "8372f3c589c8fa50ea7b127dabe008528837b11781f65bfc72d96259d49b44c5", [:mix], [{:ecto, "~> 3.2", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "61c9c2c531f87ce7e2e9e57fc60d533fe97b3a62a43c21b632b0824f0773bcbe"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"error_tracker": {:hex, :error_tracker, "0.2.2", "7635f5ed6016df10d8e63348375acb2ca411e2f6f9703ee90cc2d4262af5faec", [:mix], [{:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, ">= 0.0.0", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.6", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "b975978f64d27373d3486d7de477a699e735f8c0b1c74a7370ecb80e7ae97903"},
|
||||
@@ -46,19 +45,19 @@
|
||||
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
|
||||
"exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||
"fresh": {:hex, :fresh, "0.4.4", "9d67a1d97112e70f4dfabd63b40e4b182ef64dfa84a2d9ee175eb4e34591e9f7", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}], "hexpm", "ba21d3fa0aa77bf18ca397e4c851de7432bb3f9c170a1645a16e09e4bba54315"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
|
||||
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
|
||||
"heroicons": {:hex, :heroicons, "0.5.5", "c2bcb05a90f010df246a5a2a2b54cac15483b5de137b2ef0bead77fcdf06e21a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "2f4bf929440fecd5191ba9f40e5009b0f75dc993d765c0e4d068fcb7026d6da1"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"},
|
||||
"igniter": {:hex, :igniter, "0.3.38", "c45e285098eb8be65bcde7206e113b34be40155026e7926d390c00e39fbc38d9", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "19aa9b109cd9fc858999da0a30ad9e8e883ddff7abfa7817e3b69a711c65cd13"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
@@ -73,9 +72,9 @@
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||
"merkle_map": {:hex, :merkle_map, "0.2.1", "01a88c87a6b9fb594c67c17ebaf047ee55ffa34e74297aa583ed87148006c4c8", [:mix], [], "hexpm", "fed4d143a5c8166eee4fa2b49564f3c4eace9cb252f0a82c1613bba905b2d04d"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
@@ -89,7 +88,7 @@
|
||||
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
|
||||
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.21.5", "ff0c7fe5ceff9a56b9b0bb5a6dcfb7bc96e8afc563a3bef6ae91927de4d38b8e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "bd83c8f462222236fa85044098ba3bf57f7b7d7fd5286e6bc0060c7916f7c0d8"},
|
||||
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||
"owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"},
|
||||
"parent": {:hex, :parent, "0.12.1", "495c4386f06de0df492e0a7a7199c10323a55e9e933b27222060dd86dccd6d62", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2ab589ef1f37bfcedbfb5ecfbab93354972fb7391201b8907a866dadd20b39d1"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"pathex": {:hex, :pathex, "2.5.3", "0f2674c7cb52ae9220766cae2653b4013578349ae5ec07cb0c31b92684b3f19a", [:mix], [], "hexpm", "767aefc27d0303f583ba2064f0a49546067ab5de3c42b89f014a0ba32ea04830"},
|
||||
@@ -104,30 +103,30 @@
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.5", "f072166f87c44ffaf2b47b65c5ced8c375797830e517bfcf0a006fe7eb113911", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94abbc84df8a93a64514fc41528695d7326b6f3095e906b32f264ec4280811f3"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_content_security_policy": {:hex, :plug_content_security_policy, "0.2.1", "0a19c76307ad000b3757739c14b34b83ecccf7d0a3472e64e14797a20b62939b", [:mix], [{:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ceea10050671c0387c64526e2cb337ee08e12705c737eaed80439266df5b2e29"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"plug_dynamic": {:hex, :plug_dynamic, "1.0.0", "aecc1a6c19bb4a4d3ceb35ae85999e9ec77cf50eeead754607bc657d47478b32", [:mix], [{:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "403590330db12255755e0ce6397aaf05b000f255cfe5ea8edf70dc9d4413b99c"},
|
||||
"postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
|
||||
"postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"},
|
||||
"prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
|
||||
"qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"},
|
||||
"quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"reactor": {:hex, :reactor, "0.14.0", "8dc5d4946391010bf9fa7b58dd1e75d3c1cf97315e5489b7797cf64b82ae27a4", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9cf5068e4042791c150f0dfbc00f4f435433eb948036b44b95b940e457b35a6a"},
|
||||
"req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"},
|
||||
"reactor": {:hex, :reactor, "0.10.0", "1206113c21ba69b889e072b2c189c05a7aced523b9c3cb8dbe2dab7062cb699a", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4003c33e4c8b10b38897badea395e404d74d59a31beb30469a220f2b1ffe6457"},
|
||||
"req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"},
|
||||
"retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"},
|
||||
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
||||
"rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},
|
||||
"site_encrypt": {:hex, :site_encrypt, "0.6.0", "9b3ae2b11723b9fa9b6fbee1d137ceaa0c245015a40c3f753a4ba1e8887986d2", [:mix], [{:bandit, "~> 0.7 or ~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}, {:mint, "~> 1.4", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:parent, "~> 0.11", [hex: :parent, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:x509, "~> 0.8.8", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "16e77d0bec194b9e9d95ece4b7e5b072638e1c317d3dbe58d0c26ef3635a1e33"},
|
||||
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||
"sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"},
|
||||
"spark": {:hex, :spark, "2.3.14", "a08420d08e6e0e49d740aed3e160f1cb894ba8f6b3f5e6c63253e9df1995265c", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "af50c4ea5dd67eba822247f1c98e1d4e598cb7f6c28ccf5d002f0e0718096f4f"},
|
||||
"spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},
|
||||
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
|
||||
"sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
|
||||
"spark": {:hex, :spark, "2.2.29", "a52733ff72b05a674e48d3ca7a4172fe7bec81e9116069da8b4db19030d581d9", [:mix], [{:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "111a0dadbb27537c7629bc03ac56fcab15056ab0b9ad985084b9adcdb48836c8"},
|
||||
"spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
|
||||
"splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
|
||||
"swoosh": {:hex, :swoosh, "1.16.8", "37112d4430f7054676f7652745676a6acad0e9279d3b052fe69a58c2532a67f8", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33de50fa414c07ed55fcb28e60e72a44496e14f8753e46cbf80e7b15d1adaae2"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
@@ -136,7 +135,6 @@
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||
"telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"},
|
||||
"tesla": {:hex, :tesla, "1.11.0", "81b2b10213dddb27105ec6102d9eb0cc93d7097a918a0b1594f2dfd1a4601190", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b83ab5d4c2d202e1ea2b7e17a49f788d49a699513d7c4f08f2aef2c281be69db"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.11", "b68f3e91f74d564ae20b70d981bbf7097dde084343c14ae8a33e5b5fbb3d6f37", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "555c18c62027f45d9c80df389c3d01d86ba11014652c00be26e33b1b64e98d29"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
|
||||
|
||||
@@ -42,24 +42,6 @@ In the dynamic world of EVE Online wormhole mapping, every second counts. When a
|
||||
- Your map API token (found in map settings)
|
||||
- Basic programming knowledge for integration
|
||||
|
||||
### Server Configuration (Community Edition)
|
||||
|
||||
If you're running Wanderer Community Edition (CE), you need to enable the required features via environment variables:
|
||||
|
||||
**For SSE (Server-Sent Events):**
|
||||
```bash
|
||||
WANDERER_SSE_ENABLED=true
|
||||
```
|
||||
|
||||
**For Webhooks:**
|
||||
```bash
|
||||
WANDERER_WEBHOOKS_ENABLED=true
|
||||
```
|
||||
|
||||
Add these to your `.env` file or Docker environment configuration and restart Wanderer. Without these settings, you'll receive a 403 error when trying to access SSE streams or webhook management endpoints.
|
||||
|
||||
*Note: The public Wanderer instance at wanderer.ltd has these features enabled by default.*
|
||||
|
||||
### Authentication
|
||||
Both SSE and webhook APIs use your existing map API token for authentication. This token should be kept secure and never exposed in client-side code.
|
||||
|
||||
|
||||
@@ -81,24 +81,6 @@ You can find or generate your map's API key in the map settings within the Wande
|
||||
**Session Authentication:**
|
||||
Web clients can also use session-based authentication for interactive use, maintaining compatibility with existing browser-based integrations.
|
||||
|
||||
### Server Configuration (Community Edition)
|
||||
|
||||
If you're running Wanderer Community Edition (CE), ensure the following environment variables are configured:
|
||||
|
||||
**Required for API access:**
|
||||
```bash
|
||||
WANDERER_PUBLIC_API_DISABLED=false # Enable public API (default: false)
|
||||
```
|
||||
|
||||
**Optional features:**
|
||||
```bash
|
||||
WANDERER_SSE_ENABLED=true # Enable Server-Sent Events (default: false)
|
||||
WANDERER_WEBHOOKS_ENABLED=true # Enable webhook management (default: false)
|
||||
WANDERER_CHARACTER_API_DISABLED=false # Enable character API (default: true)
|
||||
```
|
||||
|
||||
Add these to your `.env` file or Docker environment configuration and restart Wanderer.
|
||||
|
||||
## JSON:API Features
|
||||
|
||||
### Resource Relationships
|
||||
@@ -161,7 +143,7 @@ GET /api/v1/user_activities?include=character&sort=-inserted_at&page[limit]=15&p
|
||||
The API v1 provides access to over 25 resources through the Ash Framework. Here are the primary resources:
|
||||
|
||||
### Core Resources
|
||||
- **Maps** (`/api/v1/maps/:slug`) - Map management with create, read, update, and delete operations (accessed by slug with map-specific API key; no listing endpoint)
|
||||
- **Maps** (`/api/v1/maps`) - Map management with full CRUD operations
|
||||
- **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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapSseEnabled 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
|
||||
alter table(:maps_v1) do
|
||||
add :sse_enabled, :boolean, null: false, default: false
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:maps_v1) do
|
||||
remove :sse_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,141 +0,0 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddPublicApiKeyUniqueIndex do
|
||||
@moduledoc """
|
||||
Adds a unique index on the public_api_key column of maps_v1.
|
||||
|
||||
This migration:
|
||||
1. Creates a backup table (maps_v1_api_key_backup) for data safety
|
||||
2. Backs up and clears duplicate API keys (keeping the oldest by inserted_at)
|
||||
3. Creates a unique index on public_api_key where the value is not null
|
||||
4. Allows multiple NULL values (maps without API keys)
|
||||
5. Ensures all non-NULL API keys are unique
|
||||
|
||||
The partial index (WHERE public_api_key IS NOT NULL) is used because:
|
||||
- Most maps won't have an API key set
|
||||
- We only care about uniqueness for maps that do have one
|
||||
- PostgreSQL's unique constraints on nullable columns already allow multiple NULLs,
|
||||
but a partial index is more explicit and efficient
|
||||
|
||||
## Data Recovery
|
||||
|
||||
If you need to restore cleared API keys, query the backup table:
|
||||
|
||||
SELECT map_id, old_public_api_key, backed_up_at
|
||||
FROM maps_v1_api_key_backup
|
||||
WHERE reason = 'duplicate_api_key_cleared_for_unique_index';
|
||||
|
||||
To restore a specific map's API key:
|
||||
|
||||
UPDATE maps_v1 SET public_api_key = '<old_key>'
|
||||
WHERE id = '<map_id>';
|
||||
|
||||
Note: Restoring will cause uniqueness conflicts if duplicates still exist.
|
||||
"""
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
# Create backup table before any destructive changes
|
||||
create_backup_table()
|
||||
|
||||
# Check for any duplicate non-null API keys and handle them (with backup)
|
||||
check_and_fix_duplicates()
|
||||
|
||||
# Create the unique index
|
||||
create_if_not_exists(
|
||||
index(:maps_v1, [:public_api_key],
|
||||
unique: true,
|
||||
name: :maps_v1_unique_public_api_key_index,
|
||||
where: "public_api_key IS NOT NULL"
|
||||
)
|
||||
)
|
||||
|
||||
IO.puts("Created unique index on maps_v1.public_api_key")
|
||||
end
|
||||
|
||||
defp create_backup_table do
|
||||
repo().query!(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS maps_v1_api_key_backup (
|
||||
id UUID PRIMARY KEY,
|
||||
map_id UUID NOT NULL,
|
||||
old_public_api_key TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
backed_up_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""",
|
||||
[]
|
||||
)
|
||||
|
||||
IO.puts("Created backup table maps_v1_api_key_backup")
|
||||
end
|
||||
|
||||
def down do
|
||||
drop_if_exists(index(:maps_v1, [:public_api_key], name: :maps_v1_unique_public_api_key_index))
|
||||
|
||||
IO.puts("Dropped unique index on maps_v1.public_api_key")
|
||||
|
||||
# Drop backup table
|
||||
repo().query!("DROP TABLE IF EXISTS maps_v1_api_key_backup", [])
|
||||
IO.puts("Dropped backup table maps_v1_api_key_backup")
|
||||
end
|
||||
|
||||
defp check_and_fix_duplicates do
|
||||
# Check for duplicate non-null API keys
|
||||
duplicates_query = """
|
||||
SELECT public_api_key, COUNT(*) as cnt
|
||||
FROM maps_v1
|
||||
WHERE public_api_key IS NOT NULL
|
||||
GROUP BY public_api_key
|
||||
HAVING COUNT(*) > 1
|
||||
"""
|
||||
|
||||
case repo().query(duplicates_query, []) do
|
||||
{:ok, %{rows: []}} ->
|
||||
IO.puts("No duplicate API keys found")
|
||||
|
||||
{:ok, %{rows: rows}} when length(rows) > 0 ->
|
||||
IO.puts("Found #{length(rows)} duplicate API key(s) - clearing duplicates...")
|
||||
|
||||
# For each duplicate, keep the first (by inserted_at) and clear the rest
|
||||
Enum.each(rows, fn [api_key, count] ->
|
||||
IO.puts(" Processing duplicate key (#{count} occurrences)")
|
||||
|
||||
# Get all IDs with this key, ordered by inserted_at
|
||||
ids_query = """
|
||||
SELECT id::text
|
||||
FROM maps_v1
|
||||
WHERE public_api_key = $1
|
||||
ORDER BY inserted_at ASC, id ASC
|
||||
"""
|
||||
|
||||
case repo().query(ids_query, [api_key]) do
|
||||
{:ok, %{rows: id_rows}} ->
|
||||
# Keep first, clear rest
|
||||
[_keep | clear_ids] = Enum.map(id_rows, fn [id] -> id end)
|
||||
|
||||
Enum.each(clear_ids, fn id ->
|
||||
# Backup the API key before clearing
|
||||
backup_query = """
|
||||
INSERT INTO maps_v1_api_key_backup (id, map_id, old_public_api_key, reason)
|
||||
VALUES (gen_random_uuid(), $1::uuid, $2, 'duplicate_api_key_cleared_for_unique_index')
|
||||
"""
|
||||
|
||||
repo().query!(backup_query, [id, api_key])
|
||||
|
||||
# Clear the duplicate
|
||||
clear_query = "UPDATE maps_v1 SET public_api_key = NULL WHERE id::text = $1"
|
||||
repo().query!(clear_query, [id])
|
||||
IO.puts(" Backed up and cleared API key for map #{id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
raise "Failed to get duplicate IDs for key: #{inspect(error)}"
|
||||
end
|
||||
end)
|
||||
|
||||
IO.puts("Duplicate API keys cleared")
|
||||
|
||||
{:error, error} ->
|
||||
raise "Failed to check for duplicate keys: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,216 +0,0 @@
|
||||
{
|
||||
"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": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "slug",
|
||||
"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": "personal_note",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "public_api_key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "hubs",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"wormholes\"",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "scope",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "only_tracked_characters",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "options",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "webhooks_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "sse_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"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": "maps_v1_owner_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
},
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "065F1CDA64C0B00355F9D7B692E48A5E547C69E4E66F6F327C03FBA217850B1D",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_slug_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "slug"
|
||||
}
|
||||
],
|
||||
"name": "unique_slug",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# export ERL_AFLAGS="-proto_dist inet6_tcp"
|
||||
export ERL_AFLAGS="-proto_dist inet6_tcp"
|
||||
export RELEASE_DISTRIBUTION="name"
|
||||
|
||||
# Use custom RELEASE_NODE if set, otherwise detect environment
|
||||
|
||||
@@ -56,9 +56,6 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
end
|
||||
|
||||
test "returns error when neither map_id nor slug provided", %{conn: conn} do
|
||||
# The route /api/map/acls is within the :api_map pipeline scope
|
||||
# which requires proper map authentication, but the CheckMapApiKey plug
|
||||
# specifically requires map_id or slug query parameters
|
||||
conn = get(conn, "/api/map/acls")
|
||||
assert %{"error" => _} = json_response(conn, 400)
|
||||
end
|
||||
@@ -132,19 +129,15 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
}
|
||||
}
|
||||
|
||||
# The route expects either map_id or slug as query parameter
|
||||
# Not providing either should result in a 400 error
|
||||
conn = post(conn, "/api/map/acls", acl_params)
|
||||
assert %{"error" => _} = json_response(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/acls/:id (show)" do
|
||||
# Note: show/update actions use :api_acl pipeline (CheckAclApiKey plug)
|
||||
# which validates the ACL's api_key, not the map's api_key
|
||||
# So we don't need setup_map_authentication here
|
||||
setup :setup_map_authentication
|
||||
|
||||
test "returns access list details with members", %{conn: conn} do
|
||||
test "returns access list details with members", %{conn: conn, map: map} do
|
||||
character = Factory.insert(:character, %{eve_id: "2112073677"})
|
||||
|
||||
acl =
|
||||
@@ -195,24 +188,23 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
assert "Corp Member" in member_names
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent ACL", %{conn: _conn} do
|
||||
# Create a fresh conn without any authentication setup
|
||||
conn = build_conn()
|
||||
|
||||
test "returns 404 for non-existent ACL", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer some-api-key")
|
||||
|> get(~p"/api/acls/#{Ecto.UUID.generate()}")
|
||||
|
||||
# The CheckAclApiKey plug will return 404 when the ACL ID is not found
|
||||
assert conn.status == 404
|
||||
# The response might not be JSON if auth fails first
|
||||
case conn.status do
|
||||
404 -> assert conn.status == 404
|
||||
# Other auth-related errors are acceptable
|
||||
_ -> assert conn.status in [400, 401, 404]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/acls/:id (update)" do
|
||||
# Note: update action uses :api_acl pipeline (CheckAclApiKey plug)
|
||||
# which validates the ACL's api_key, not the map's api_key
|
||||
# So we don't need setup_map_authentication here
|
||||
setup :setup_map_authentication
|
||||
|
||||
test "updates access list attributes", %{conn: conn} do
|
||||
character = Factory.insert(:character, %{eve_id: "2112073677"})
|
||||
@@ -252,8 +244,7 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
assert updated_acl.description == "Updated description"
|
||||
end
|
||||
|
||||
test "preserves api_key on update", %{conn: _conn} do
|
||||
conn = build_conn()
|
||||
test "preserves api_key on update", %{conn: conn} do
|
||||
character = Factory.insert(:character, %{eve_id: "2112073677"})
|
||||
|
||||
original_api_key = "original-api-key"
|
||||
@@ -283,8 +274,7 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
} = json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent ACL", %{conn: _conn} do
|
||||
conn = build_conn()
|
||||
test "returns 404 for non-existent ACL", %{conn: conn} do
|
||||
update_params = %{
|
||||
"acl" => %{
|
||||
"name" => "Updated Name"
|
||||
@@ -296,12 +286,15 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
|> put_req_header("authorization", "Bearer some-api-key")
|
||||
|> put(~p"/api/acls/#{Ecto.UUID.generate()}", update_params)
|
||||
|
||||
# The CheckAclApiKey plug will return 404 when the ACL ID is not found
|
||||
assert conn.status == 404
|
||||
# The response might not be JSON if auth fails first
|
||||
case conn.status do
|
||||
404 -> assert conn.status == 404
|
||||
# Other auth-related errors are acceptable
|
||||
_ -> assert conn.status in [400, 401, 404]
|
||||
end
|
||||
end
|
||||
|
||||
test "validates update parameters", %{conn: _conn} do
|
||||
conn = build_conn()
|
||||
test "validates update parameters", %{conn: conn} do
|
||||
character = Factory.insert(:character, %{eve_id: "2112073677"})
|
||||
|
||||
acl =
|
||||
|
||||
@@ -8,8 +8,10 @@ defmodule WandererAppWeb.AccessListMemberAPIControllerTest do
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
# Mocks are already in global mode from application startup
|
||||
# No need to call Mox.set_mox_global() again
|
||||
# Ensure we're in global mode and re-setup mocks
|
||||
# This ensures all processes can access the mocks
|
||||
Mox.set_mox_global()
|
||||
WandererApp.Test.Mocks.setup_additional_expectations()
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule WandererAppWeb.CommonAPIControllerTest do
|
||||
use WandererAppWeb.ApiCase, async: false
|
||||
use WandererAppWeb.ApiCase, async: true
|
||||
|
||||
describe "GET /api/common/system-static-info" do
|
||||
test "returns system static info for valid system ID", %{conn: conn} do
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
defmodule WandererAppWeb.MapAuditAPIControllerIntegrationTest do
|
||||
use WandererAppWeb.ApiCase, async: false
|
||||
use WandererAppWeb.ApiCase
|
||||
|
||||
alias WandererAppWeb.Factory
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user