mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
feat(Api): added map audit base API. Added comments server validations.
This commit is contained in:
@@ -1,30 +1,21 @@
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { Comments } from '@/hooks/Mapper/components/mapInterface/components/Comments';
|
||||
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { InfoDrawer, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useRef } from 'react';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import { COMPACT_MAX_WIDTH } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import clsx from 'clsx';
|
||||
import { CommentsEditor } from '@/hooks/Mapper/components/mapInterface/components/CommentsEditor';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
export const CommentsWidgetContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, isSubscriptionActive },
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
if (!isSubscriptionActive) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Comments available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNotSelectedSystem) {
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
|
||||
@@ -46,7 +37,7 @@ export const CommentsWidget = () => {
|
||||
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
data: { selectedSystems, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
const [systemId] = selectedSystems;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
@@ -54,7 +45,8 @@ export const CommentsWidget = () => {
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div ref={containerRef} className="flex items-center gap-1 text-xs w-full">
|
||||
<div ref={containerRef} className="flex justify-between items-center gap-1 text-xs w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isCompact && (
|
||||
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
|
||||
Comments {isNotSelectedSystem ? '' : 'in'}
|
||||
@@ -62,6 +54,26 @@ export const CommentsWidget = () => {
|
||||
)}
|
||||
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
content: (
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add/delete comment?</b>}>
|
||||
It is possible to use markdown formating. <br />
|
||||
Only users with tracking permission can add/delete comments. <br />
|
||||
</InfoDrawer>
|
||||
<InfoDrawer title={<b className="text-slate-50">Limitations</b>}>
|
||||
Each comment length is limited to <b>500</b> characters. <br />
|
||||
No more than <b>{isSubscriptionActive ? '500' : '30'}</b> comments are allowed per system*. <br />
|
||||
<small>* based on active map subscription.</small>
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CommentsWidgetContent />
|
||||
|
||||
@@ -180,7 +180,8 @@ 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
|
||||
|
||||
has_access = is_owner or is_acl_owner or is_member_eve or is_member_corp or 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)
|
||||
@@ -275,8 +276,8 @@ defmodule WandererApp.Maps do
|
||||
def get_system_comments_activity(system_id) do
|
||||
from(sc in WandererApp.Api.MapSystemComment,
|
||||
where: sc.system_id == ^system_id,
|
||||
group_by: [sc.id],
|
||||
select: {count(sc.id)}
|
||||
group_by: [sc.system_id],
|
||||
select: {count(sc.system_id)}
|
||||
)
|
||||
|> WandererApp.Repo.all()
|
||||
end
|
||||
|
||||
172
lib/wanderer_app_web/controllers/map_audit_api_controller.ex
Normal file
172
lib/wanderer_app_web/controllers/map_audit_api_controller.ex
Normal file
@@ -0,0 +1,172 @@
|
||||
defmodule WandererAppWeb.MapAuditAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
import Ash.Query, only: [filter: 2]
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.Character
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
|
||||
alias WandererAppWeb.UtilAPIController, as: Util
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Inline Schemas
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@character_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_id: %OpenApiSpex.Schema{type: :string},
|
||||
corporation_ticker: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_id: %OpenApiSpex.Schema{type: :string},
|
||||
alliance_ticker: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["eve_id", "name"]
|
||||
}
|
||||
|
||||
@map_audit_event_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
entity_type: %OpenApiSpex.Schema{type: :string},
|
||||
event_name: %OpenApiSpex.Schema{type: :string},
|
||||
event_data: %OpenApiSpex.Schema{type: :string},
|
||||
character: @character_schema,
|
||||
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
|
||||
},
|
||||
required: ["entity_type", "event_name", "event_data", "inserted_at"]
|
||||
}
|
||||
|
||||
@map_audit_response_schema %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %OpenApiSpex.Schema{
|
||||
type: :array,
|
||||
items: @map_audit_event_schema
|
||||
}
|
||||
},
|
||||
required: ["data"]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# MAP endpoints
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
GET /api/map/audit
|
||||
|
||||
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
|
||||
|
||||
Examples:
|
||||
GET /api/map/audit?map_id=466e922b-e758-485e-9b86-afae06b88363&period=1H
|
||||
GET /api/map/audit?slug=my-unique-wormhole-map&period=1H
|
||||
"""
|
||||
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||
operation(:index,
|
||||
summary: "List Map Audit events",
|
||||
description:
|
||||
"Lists all audit events for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
|
||||
parameters: [
|
||||
map_id: [
|
||||
in: :query,
|
||||
description: "Map identifier (UUID) - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: ""
|
||||
],
|
||||
slug: [
|
||||
in: :query,
|
||||
description: "Map slug - Either map_id or slug must be provided",
|
||||
type: :string,
|
||||
required: false,
|
||||
example: "map-name"
|
||||
],
|
||||
period: [
|
||||
in: :query,
|
||||
description: "Activity period (1H, 1D, 1W, 1M, 2M, 3M)",
|
||||
type: :string,
|
||||
required: true,
|
||||
example: "1D"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {
|
||||
"List of map audit events",
|
||||
"application/json",
|
||||
@map_audit_response_schema
|
||||
},
|
||||
bad_request:
|
||||
{"Error", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{type: :string}
|
||||
},
|
||||
required: ["error"],
|
||||
example: %{
|
||||
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG"
|
||||
}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
def index(conn, params) do
|
||||
with {:ok, map_id} <- Util.fetch_map_id(params),
|
||||
{:ok, period} <- Util.require_param(params, "period"),
|
||||
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
||||
{:ok, data} <-
|
||||
Api.read(query) do
|
||||
data = Enum.map(data, &map_audit_event_to_json/1)
|
||||
json(conn, %{data: data})
|
||||
else
|
||||
{:error, msg} when is_binary(msg) ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: msg})
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Request failed: #{inspect(reason)}"})
|
||||
end
|
||||
end
|
||||
|
||||
defp map_audit_event_to_json(
|
||||
%{event_type: event_type, event_data: event_data, character: character} = event
|
||||
) do
|
||||
# Start with the basic system data
|
||||
result =
|
||||
Map.take(event, [
|
||||
:entity_type,
|
||||
:inserted_at
|
||||
])
|
||||
|
||||
result
|
||||
|> Map.put(:character, WandererAppWeb.MapEventHandler.map_ui_character_stat(character))
|
||||
|> Map.put(:event_name, WandererAppWeb.UserActivity.get_event_name(event_type))
|
||||
|> Map.put(
|
||||
:event_data,
|
||||
WandererAppWeb.UserActivity.get_event_data(
|
||||
event_type,
|
||||
Jason.decode!(event_data) |> Map.drop(["character_id"])
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp get_original_system_name(solar_system_id) do
|
||||
# Fetch the original system name from the MapSolarSystem resource
|
||||
case WandererApp.Api.MapSolarSystem.by_solar_system_id(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
system.solar_system_name
|
||||
|
||||
_error ->
|
||||
"Unknown System"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -54,7 +54,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
</p>
|
||||
|
||||
<p class="text-sm text-[var(--color-gray-4)] w-[15%]">
|
||||
<%= get_event_name(@activity.event_type) %>
|
||||
{get_event_name(@activity.event_type)}
|
||||
</p>
|
||||
<.activity_event event_type={@activity.event_type} event_data={@activity.event_data} />
|
||||
|
||||
@@ -82,7 +82,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
|
||||
</div>
|
||||
</div>
|
||||
<%= @character.name %>
|
||||
{@character.name}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -95,7 +95,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
<div class="w-[40%]">
|
||||
<div class="flex items-center gap-1">
|
||||
<h6 class="text-base leading-[150%] font-semibold dark:text-white">
|
||||
<%= get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
|
||||
{get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"]))}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,26 +109,26 @@ defmodule WandererAppWeb.UserActivity do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp get_event_name(:hub_added), do: "Hub Added"
|
||||
defp get_event_name(:hub_removed), do: "Hub Removed"
|
||||
defp get_event_name(:map_connection_added), do: "Connection Added"
|
||||
defp get_event_name(:map_connection_updated), do: "Connection Updated"
|
||||
defp get_event_name(:map_connection_removed), do: "Connection Removed"
|
||||
defp get_event_name(:map_acl_added), do: "Acl Added"
|
||||
defp get_event_name(:map_acl_removed), do: "Acl Removed"
|
||||
defp get_event_name(:system_added), do: "System Added"
|
||||
defp get_event_name(:system_updated), do: "System Updated"
|
||||
defp get_event_name(:systems_removed), do: "System(s) Removed"
|
||||
defp get_event_name(:signatures_added), do: "Signatures Added"
|
||||
defp get_event_name(:signatures_removed), do: "Signatures Removed"
|
||||
defp get_event_name(name), do: name
|
||||
def get_event_name(:hub_added), do: "Hub Added"
|
||||
def get_event_name(:hub_removed), do: "Hub Removed"
|
||||
def get_event_name(:map_connection_added), do: "Connection Added"
|
||||
def get_event_name(:map_connection_updated), do: "Connection Updated"
|
||||
def get_event_name(:map_connection_removed), do: "Connection Removed"
|
||||
def get_event_name(:map_acl_added), do: "Acl Added"
|
||||
def get_event_name(:map_acl_removed), do: "Acl Removed"
|
||||
def get_event_name(:system_added), do: "System Added"
|
||||
def get_event_name(:system_updated), do: "System Updated"
|
||||
def get_event_name(:systems_removed), do: "System(s) Removed"
|
||||
def get_event_name(:signatures_added), do: "Signatures Added"
|
||||
def get_event_name(:signatures_removed), do: "Signatures Removed"
|
||||
def get_event_name(name), do: name
|
||||
|
||||
defp get_event_data(:map_acl_added, %{"acl_id" => acl_id}) do
|
||||
def get_event_data(:map_acl_added, %{"acl_id" => acl_id}) do
|
||||
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
|
||||
"#{acl.name}"
|
||||
end
|
||||
|
||||
defp get_event_data(:map_acl_removed, %{"acl_id" => acl_id}) do
|
||||
def get_event_data(:map_acl_removed, %{"acl_id" => acl_id}) do
|
||||
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
|
||||
"#{acl.name}"
|
||||
end
|
||||
@@ -137,7 +137,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
# defp get_event_data(:system_added, data), do: data
|
||||
#
|
||||
|
||||
defp get_event_data(:system_updated, %{
|
||||
def get_event_data(:system_updated, %{
|
||||
"key" => "labels",
|
||||
"solar_system_id" => solar_system_id,
|
||||
"value" => value
|
||||
@@ -154,22 +154,22 @@ defmodule WandererAppWeb.UserActivity do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_event_data(:system_added, %{
|
||||
def get_event_data(:system_added, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:hub_added, %{
|
||||
def get_event_data(:hub_added, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:hub_removed, %{
|
||||
def get_event_data(:hub_removed, %{
|
||||
"solar_system_id" => solar_system_id
|
||||
}),
|
||||
do: get_system_name(solar_system_id)
|
||||
|
||||
defp get_event_data(:system_updated, %{
|
||||
def get_event_data(:system_updated, %{
|
||||
"key" => key,
|
||||
"solar_system_id" => solar_system_id,
|
||||
"value" => value
|
||||
@@ -178,7 +178,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
"#{system_name}: #{key} - #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp get_event_data(:systems_removed, %{
|
||||
def get_event_data(:systems_removed, %{
|
||||
"solar_system_ids" => solar_system_ids
|
||||
}),
|
||||
do:
|
||||
@@ -186,20 +186,20 @@ defmodule WandererAppWeb.UserActivity do
|
||||
|> Enum.map(&get_system_name/1)
|
||||
|> Enum.join(", ")
|
||||
|
||||
defp get_event_data(signatures_event, %{
|
||||
def get_event_data(signatures_event, %{
|
||||
"solar_system_id" => solar_system_id,
|
||||
"signatures" => signatures
|
||||
})
|
||||
when signatures_event in [:signatures_added, :signatures_removed],
|
||||
do: "#{get_system_name(solar_system_id)}: #{signatures |> Enum.join(", ")}"
|
||||
|
||||
defp get_event_data(signatures_event, %{
|
||||
def get_event_data(signatures_event, %{
|
||||
"signatures" => signatures
|
||||
})
|
||||
when signatures_event in [:signatures_added, :signatures_removed],
|
||||
do: signatures |> Enum.join(", ")
|
||||
|
||||
defp get_event_data(:map_connection_added, %{
|
||||
def get_event_data(:map_connection_added, %{
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id
|
||||
}) do
|
||||
@@ -208,7 +208,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
"[#{source_system_name}:#{target_system_name}]"
|
||||
end
|
||||
|
||||
defp get_event_data(:map_connection_removed, %{
|
||||
def get_event_data(:map_connection_removed, %{
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id
|
||||
}) do
|
||||
@@ -217,7 +217,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
"[#{source_system_name}:#{target_system_name}]"
|
||||
end
|
||||
|
||||
defp get_event_data(:map_connection_updated, %{
|
||||
def get_event_data(:map_connection_updated, %{
|
||||
"key" => key,
|
||||
"solar_system_source_id" => solar_system_source_id,
|
||||
"solar_system_target_id" => solar_system_target_id,
|
||||
@@ -228,7 +228,7 @@ defmodule WandererAppWeb.UserActivity do
|
||||
"[#{source_system_name}:#{target_system_name}] #{key} - #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp get_event_data(_name, data), do: Jason.encode!(data)
|
||||
def get_event_data(_name, data), do: Jason.encode!(data)
|
||||
|
||||
defp get_system_name(solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
|
||||
@@ -44,23 +44,51 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
current_user: current_user,
|
||||
has_tracked_characters?: true,
|
||||
map_id: map_id,
|
||||
is_subscription_active?: is_subscription_active?,
|
||||
tracked_character_ids: tracked_character_ids,
|
||||
user_permissions: %{add_system: true}
|
||||
}
|
||||
} =
|
||||
socket
|
||||
) do
|
||||
system =
|
||||
WandererApp.Map.find_system_by_location(map_id, %{
|
||||
solar_system_id: solar_system_id |> String.to_integer()
|
||||
})
|
||||
|
||||
comments_count =
|
||||
system.id
|
||||
|> WandererApp.Maps.get_system_comments_activity()
|
||||
|> case do
|
||||
[{count}] when not is_nil(count) ->
|
||||
count
|
||||
|
||||
_ ->
|
||||
0
|
||||
end
|
||||
|
||||
cond do
|
||||
(is_subscription_active? && comments_count < 500) || comments_count < 30 ->
|
||||
map_id
|
||||
|> WandererApp.Map.Server.add_system_comment(
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
text: text
|
||||
text: text |> String.slice(0..500)
|
||||
},
|
||||
current_user.id,
|
||||
tracked_character_ids |> List.first()
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
true ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> Phoenix.LiveView.put_flash(
|
||||
:error,
|
||||
"Your reach the maximum number of comments available. Please remove some comments before adding new ones."
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
@@ -116,6 +144,9 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
def map_system_comment(nil), do: nil
|
||||
|
||||
def map_system_comment(
|
||||
@@ -114,9 +114,11 @@ defmodule WandererAppWeb.Router do
|
||||
http_url = URI.to_string(home_url)
|
||||
|
||||
# Only add script-src-elem when in development mode
|
||||
script_src_elem = if(@code_reloading, do:
|
||||
@script_src_values ++ [ws_url, http_url],
|
||||
else: @script_src_values)
|
||||
script_src_elem =
|
||||
if(@code_reloading,
|
||||
do: @script_src_values ++ [ws_url, http_url],
|
||||
else: @script_src_values
|
||||
)
|
||||
|
||||
directives = %{
|
||||
default_src: ~w('none'),
|
||||
@@ -203,6 +205,7 @@ defmodule WandererAppWeb.Router do
|
||||
|
||||
scope "/api/map", WandererAppWeb do
|
||||
pipe_through [:api, :api_map]
|
||||
get "/audit", MapAuditAPIController, :index
|
||||
get "/systems", MapAPIController, :list_systems
|
||||
get "/system", MapAPIController, :show_system
|
||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||
|
||||
Reference in New Issue
Block a user