mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 10:45:54 +00:00
feat: add api for acl management (#171)
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wanderer-dev",
|
"name": "wanderer-dev",
|
||||||
"dockerComposeFile": ["./docker-compose.yml"],
|
"dockerComposeFile": ["./docker-compose.yml"],
|
||||||
"extensions": ["jakebecker.elixir-ls"],
|
"extensions": [
|
||||||
|
"jakebecker.elixir-ls",
|
||||||
|
"JakeBecker.elixir-ls",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
],
|
||||||
"service": "wanderer",
|
"service": "wanderer",
|
||||||
"workspaceFolder": "/app",
|
"workspaceFolder": "/app",
|
||||||
"shutdownAction": "stopCompose",
|
"shutdownAction": "stopCompose",
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
|
|||||||
export GIT_SHA="1111"
|
export GIT_SHA="1111"
|
||||||
export WANDERER_INVITES="false"
|
export WANDERER_INVITES="false"
|
||||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||||
|
export WANDERER_CHARACTER_API_DISABLED="false"
|
||||||
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
||||||
|
|||||||
BIN
assets/static/images/news/02-20-acl-api/generate-acl-key.png
Executable file
BIN
assets/static/images/news/02-20-acl-api/generate-acl-key.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -53,6 +53,11 @@ public_api_disabled =
|
|||||||
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|
||||||
|> String.to_existing_atom()
|
|> String.to_existing_atom()
|
||||||
|
|
||||||
|
character_api_disabled =
|
||||||
|
config_dir
|
||||||
|
|> get_var_from_path_or_env("WANDERER_CHARACTER_API_DISABLED", "false")
|
||||||
|
|> String.to_existing_atom()
|
||||||
|
|
||||||
zkill_preload_disabled =
|
zkill_preload_disabled =
|
||||||
config_dir
|
config_dir
|
||||||
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|
||||||
@@ -123,6 +128,7 @@ config :wanderer_app,
|
|||||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||||
public_api_disabled: public_api_disabled,
|
public_api_disabled: public_api_disabled,
|
||||||
|
character_api_disabled: character_api_disabled,
|
||||||
zkill_preload_disabled: zkill_preload_disabled,
|
zkill_preload_disabled: zkill_preload_disabled,
|
||||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
|
|
||||||
code_interface do
|
code_interface do
|
||||||
define(:create, action: :create)
|
define(:create, action: :create)
|
||||||
|
|
||||||
define(:available, action: :available)
|
define(:available, action: :available)
|
||||||
define(:new, action: :new)
|
define(:new, action: :new)
|
||||||
define(:read, action: :read)
|
define(:read, action: :read)
|
||||||
@@ -39,7 +38,8 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [:name, :description, :owner_id]
|
# Added :api_key to the accepted attributes
|
||||||
|
accept [:name, :description, :owner_id, :api_key]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
argument :owner_id, :uuid, allow_nil?: false
|
argument :owner_id, :uuid, allow_nil?: false
|
||||||
@@ -48,7 +48,7 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
end
|
end
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
accept [:name, :description, :owner_id]
|
accept [:name, :description, :owner_id, :api_key]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -68,6 +68,10 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attribute :api_key, :string do
|
||||||
|
allow_nil? true
|
||||||
|
end
|
||||||
|
|
||||||
create_timestamp(:inserted_at)
|
create_timestamp(:inserted_at)
|
||||||
update_timestamp(:updated_at)
|
update_timestamp(:updated_at)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ defmodule WandererApp.Env do
|
|||||||
def invites, do: get_key(:invites, false)
|
def invites, do: get_key(:invites, false)
|
||||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||||
|
def character_api_disabled?, do: get_key(:character_api_disabled, false)
|
||||||
def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
|
def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
|
||||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||||
def admins, do: get_key(:admins, [])
|
def admins, do: get_key(:admins, [])
|
||||||
|
|||||||
181
lib/wanderer_app_web/controllers/access_list_api_controller.ex
Normal file
181
lib/wanderer_app_web/controllers/access_list_api_controller.ex
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
defmodule WandererAppWeb.MapAccessListAPIController do
|
||||||
|
@moduledoc """
|
||||||
|
API endpoints for managing Access Lists.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- GET /api/map/acls?map_id=... or ?slug=... (list ACLs)
|
||||||
|
- POST /api/map/acls (create ACL)
|
||||||
|
- GET /api/acls/:id (show ACL)
|
||||||
|
- PUT /api/acls/:id (update ACL)
|
||||||
|
|
||||||
|
ACL members are managed via a separate controller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
alias WandererApp.Api.{AccessList, Character}
|
||||||
|
# Do not alias Map—to avoid conflicts—use the full module name: WandererApp.Map.
|
||||||
|
alias WandererAppWeb.UtilAPIController, as: Util
|
||||||
|
import Ash.Query
|
||||||
|
|
||||||
|
# List ACLs for a given map (returns reduced info: no api_key, no members, and includes owner_eve_id)
|
||||||
|
def index(conn, params) do
|
||||||
|
case Util.fetch_map_id(params) do
|
||||||
|
{:ok, map_identifier} ->
|
||||||
|
with {:ok, map} <- get_map(map_identifier) do
|
||||||
|
acls = map.acls || []
|
||||||
|
json(conn, %{data: Enum.map(acls, &acl_to_list_json/1)})
|
||||||
|
else
|
||||||
|
{:error, :map_not_found} ->
|
||||||
|
conn |> put_status(:not_found) |> json(%{error: "Map not found"})
|
||||||
|
{:error, error} ->
|
||||||
|
conn |> put_status(:internal_server_error) |> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
{:error, msg} ->
|
||||||
|
conn |> put_status(:bad_request) |> json(%{error: msg})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new ACL for a map
|
||||||
|
def create(conn, params) do
|
||||||
|
with {:ok, map_identifier} <- Util.fetch_map_id(params),
|
||||||
|
{:ok, map} <- get_map(map_identifier),
|
||||||
|
%{"acl" => acl_params} <- params,
|
||||||
|
owner_eve_id when is_binary(owner_eve_id) <- Map.get(acl_params, "owner_eve_id"),
|
||||||
|
{:ok, character} <- find_character_by_eve_id(owner_eve_id),
|
||||||
|
{:ok, new_api_key} <- {:ok, UUID.uuid4()},
|
||||||
|
{:ok, new_params} <- {:ok,
|
||||||
|
acl_params
|
||||||
|
|> Map.delete("owner_eve_id")
|
||||||
|
|> Map.put("owner_id", character.id)
|
||||||
|
|> Map.put("api_key", new_api_key)
|
||||||
|
},
|
||||||
|
{:ok, new_acl} <- AccessList.new(new_params),
|
||||||
|
{:ok, _} <- {:ok, associate_acl_with_map(map, new_acl)}
|
||||||
|
do
|
||||||
|
json(conn, %{data: acl_to_json(new_acl)})
|
||||||
|
else
|
||||||
|
error ->
|
||||||
|
conn |> put_status(:bad_request) |> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show a specific ACL (with members)
|
||||||
|
def show(conn, %{"id" => id}) do
|
||||||
|
query = AccessList |> Ash.Query.new() |> filter(id == ^id)
|
||||||
|
case WandererApp.Api.read(query) do
|
||||||
|
{:ok, [acl]} ->
|
||||||
|
case Ash.load(acl, :members) do
|
||||||
|
{:ok, loaded_acl} -> json(conn, %{data: acl_to_json(loaded_acl)})
|
||||||
|
{:error, error} -> conn |> put_status(:internal_server_error) |> json(%{error: "Failed to load ACL members: #{inspect(error)}"})
|
||||||
|
end
|
||||||
|
{:ok, []} ->
|
||||||
|
conn |> put_status(:not_found) |> json(%{error: "ACL not found"})
|
||||||
|
{:error, error} ->
|
||||||
|
conn |> put_status(:internal_server_error) |> json(%{error: "Error reading ACL: #{inspect(error)}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update an ACL (if needed)
|
||||||
|
def update(conn, %{"id" => id, "acl" => acl_params}) do
|
||||||
|
with {:ok, acl} <- AccessList.by_id(id),
|
||||||
|
{:ok, updated_acl} <- AccessList.update(acl, acl_params),
|
||||||
|
{:ok, updated_acl} <- Ash.load(updated_acl, :members) do
|
||||||
|
json(conn, %{data: acl_to_json(updated_acl)})
|
||||||
|
else
|
||||||
|
{:error, error} ->
|
||||||
|
conn |> put_status(:bad_request) |> json(%{error: "Failed to update ACL: #{inspect(error)}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to get the map (using your module WandererApp.Map)
|
||||||
|
defp get_map(map_identifier) do
|
||||||
|
# Assuming Util.fetch_map_id returns a map id.
|
||||||
|
case WandererApp.Map.get_map(map_identifier) do
|
||||||
|
{:ok, map} -> {:ok, map}
|
||||||
|
other -> other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to convert an ACL to full JSON (for detail views)
|
||||||
|
defp acl_to_json(acl) do
|
||||||
|
members =
|
||||||
|
case acl.members do
|
||||||
|
%Ash.NotLoaded{} -> []
|
||||||
|
list when is_list(list) -> Enum.map(list, &member_to_json/1)
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
%{
|
||||||
|
id: acl.id,
|
||||||
|
name: acl.name,
|
||||||
|
description: acl.description,
|
||||||
|
owner_id: acl.owner_id,
|
||||||
|
api_key: acl.api_key,
|
||||||
|
inserted_at: acl.inserted_at,
|
||||||
|
updated_at: acl.updated_at,
|
||||||
|
members: members
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp acl_to_list_json(acl) do
|
||||||
|
full_acl =
|
||||||
|
case AccessList.by_id(acl.id) do
|
||||||
|
{:ok, loaded_acl} -> loaded_acl
|
||||||
|
_ -> acl
|
||||||
|
end
|
||||||
|
|
||||||
|
owner_eve_id =
|
||||||
|
case find_character_by_id(full_acl.owner_id) do
|
||||||
|
{:ok, character} -> character.eve_id
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
id: full_acl.id,
|
||||||
|
name: full_acl.name,
|
||||||
|
description: full_acl.description,
|
||||||
|
owner_eve_id: owner_eve_id,
|
||||||
|
inserted_at: full_acl.inserted_at,
|
||||||
|
updated_at: full_acl.updated_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp member_to_json(member) do
|
||||||
|
%{
|
||||||
|
id: member.id,
|
||||||
|
name: member.name,
|
||||||
|
role: member.role,
|
||||||
|
eve_character_id: member.eve_character_id,
|
||||||
|
inserted_at: member.inserted_at,
|
||||||
|
updated_at: member.updated_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to find a character by external EVE id (used in create action)
|
||||||
|
defp find_character_by_eve_id(eve_id) do
|
||||||
|
query = Character |> Ash.Query.new() |> filter(eve_id == ^eve_id)
|
||||||
|
case WandererApp.Api.read(query) do
|
||||||
|
{:ok, [character]} -> {:ok, character}
|
||||||
|
{:ok, []} -> {:error, "owner_eve_id does not match any existing character"}
|
||||||
|
other -> other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to find a character by internal id (used in acl_to_list_json)
|
||||||
|
defp find_character_by_id(id) do
|
||||||
|
query = Character |> Ash.Query.new() |> filter(id == ^id)
|
||||||
|
case WandererApp.Api.read(query) do
|
||||||
|
{:ok, [character]} -> {:ok, character}
|
||||||
|
{:ok, []} -> {:error, "Character not found"}
|
||||||
|
other -> other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Associate the new ACL with the map by updating the map's acls list.
|
||||||
|
defp associate_acl_with_map(map, new_acl) do
|
||||||
|
current_acls = map.acls || []
|
||||||
|
updated_acls = current_acls ++ [new_acl]
|
||||||
|
case WandererApp.Map.update_map(map.map_id, %{acls: updated_acls}) do
|
||||||
|
_ -> :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||||
|
@moduledoc """
|
||||||
|
Handles creation, role updates, and deletion of individual ACL members.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
alias WandererApp.Api.{AccessListMember, Character}
|
||||||
|
import Ash.Query
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
POST /api/acls/:acl_id/members
|
||||||
|
|
||||||
|
Creates a new member for the given ACL.
|
||||||
|
|
||||||
|
Request Body example:
|
||||||
|
{
|
||||||
|
"member": {
|
||||||
|
"eve_character_id": "CHARACTER_EXTERNAL_EVE_ID",
|
||||||
|
"role": "viewer" // optional; defaults to "viewer" if not provided
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
The controller looks up the character by filtering on its external EVE ID (eve_id),
|
||||||
|
injects the character's name into the membership, and creates the membership record.
|
||||||
|
"""
|
||||||
|
def create(conn, %{"acl_id" => acl_id, "member" => member_params}) do
|
||||||
|
with eve_id when not is_nil(eve_id) <- Map.get(member_params, "eve_character_id"),
|
||||||
|
# Build a query to find the character by its external EVE id (eve_id)
|
||||||
|
query = Character |> Ash.Query.new() |> filter(eve_id == ^eve_id),
|
||||||
|
{:ok, characters} <- WandererApp.Api.read(query),
|
||||||
|
[character] <- characters do
|
||||||
|
# Inject the looked-up name into the parameters.
|
||||||
|
member_params = Map.put(member_params, "name", character.name)
|
||||||
|
# Merge in the ACL id so that Ash knows which ACL the member belongs to.
|
||||||
|
merged_params = Map.put(member_params, "access_list_id", acl_id)
|
||||||
|
|
||||||
|
case AccessListMember.create(merged_params) do
|
||||||
|
{:ok, new_member} ->
|
||||||
|
json(conn, %{data: member_to_json(new_member)})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Failed to create member: #{inspect(error)}"})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Missing eve_character_id in member payload"})
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Character not found"})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Character lookup failed: #{inspect(error)}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
PUT /api/acls/:acl_id/members/:member_id
|
||||||
|
|
||||||
|
Updates a single ACL member’s role based on the external EVE ID provided in the URL.
|
||||||
|
|
||||||
|
Request Body example:
|
||||||
|
{
|
||||||
|
"member": {
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
def update_role(conn, %{"acl_id" => acl_id, "member_id" => eve_id, "member" => member_params}) do
|
||||||
|
membership_query =
|
||||||
|
AccessListMember
|
||||||
|
|> Ash.Query.new()
|
||||||
|
|> filter(eve_character_id == ^eve_id)
|
||||||
|
|> filter(access_list_id == ^acl_id)
|
||||||
|
|
||||||
|
case WandererApp.Api.read(membership_query) do
|
||||||
|
{:ok, [membership]} ->
|
||||||
|
case AccessListMember.update_role(membership, member_params) do
|
||||||
|
{:ok, updated_membership} ->
|
||||||
|
json(conn, %{data: member_to_json(updated_membership)})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Membership not found for given ACL and eve_character_id"})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
DELETE /api/acls/:acl_id/members/:member_id
|
||||||
|
|
||||||
|
Deletes a member from an ACL based on the external EVE ID provided in the URL.
|
||||||
|
"""
|
||||||
|
def delete(conn, %{"acl_id" => acl_id, "member_id" => eve_id}) do
|
||||||
|
membership_query =
|
||||||
|
AccessListMember
|
||||||
|
|> Ash.Query.new()
|
||||||
|
|> filter(eve_character_id == ^eve_id)
|
||||||
|
|> filter(access_list_id == ^acl_id)
|
||||||
|
|
||||||
|
case WandererApp.Api.read(membership_query) do
|
||||||
|
{:ok, [membership]} ->
|
||||||
|
case AccessListMember.destroy(membership) do
|
||||||
|
:ok ->
|
||||||
|
json(conn, %{ok: true})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Membership not found for given ACL and eve_character_id"})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Private Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
defp member_to_json(member) do
|
||||||
|
%{
|
||||||
|
id: member.id,
|
||||||
|
name: member.name,
|
||||||
|
role: member.role,
|
||||||
|
eve_character_id: member.eve_character_id,
|
||||||
|
inserted_at: member.inserted_at,
|
||||||
|
updated_at: member.updated_at
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
39
lib/wanderer_app_web/controllers/character_api_controller.ex
Normal file
39
lib/wanderer_app_web/controllers/character_api_controller.ex
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
defmodule WandererAppWeb.CharactersAPIController do
|
||||||
|
@moduledoc """
|
||||||
|
Exposes an endpoint for listing ALL characters in the database
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
GET /api/characters
|
||||||
|
"""
|
||||||
|
|
||||||
|
use WandererAppWeb, :controller
|
||||||
|
alias WandererApp.Api.Character
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
GET /api/characters
|
||||||
|
|
||||||
|
Lists ALL characters in the database
|
||||||
|
Returns an array of objects, each with `id`, `eve_id`, `name`, etc.
|
||||||
|
"""
|
||||||
|
def index(conn, _params) do
|
||||||
|
case WandererApp.Api.read(Character) do
|
||||||
|
{:ok, characters} ->
|
||||||
|
result =
|
||||||
|
characters
|
||||||
|
|> Enum.map(&%{
|
||||||
|
id: &1.id,
|
||||||
|
eve_id: &1.eve_id,
|
||||||
|
name: &1.name,
|
||||||
|
corporation_name: &1.corporation_name,
|
||||||
|
alliance_name: &1.alliance_name
|
||||||
|
})
|
||||||
|
|
||||||
|
json(conn, %{data: result})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: inspect(error)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
55
lib/wanderer_app_web/controllers/plugs/check_acl_api_key.ex
Normal file
55
lib/wanderer_app_web/controllers/plugs/check_acl_api_key.ex
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
defmodule WandererAppWeb.Plugs.CheckAclApiKey do
|
||||||
|
@moduledoc """
|
||||||
|
A plug that checks the "Authorization: Bearer <token>" header
|
||||||
|
against the ACL’s stored api_key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Plug.Conn
|
||||||
|
alias WandererApp.Repo
|
||||||
|
alias WandererApp.Api.AccessList
|
||||||
|
|
||||||
|
def init(opts), do: opts
|
||||||
|
|
||||||
|
def call(conn, _opts) do
|
||||||
|
header = get_req_header(conn, "authorization") |> List.first()
|
||||||
|
|
||||||
|
case header do
|
||||||
|
"Bearer " <> incoming_token ->
|
||||||
|
acl_id = conn.params["id"] || conn.params["acl_id"]
|
||||||
|
|
||||||
|
if acl_id do
|
||||||
|
case Repo.get(AccessList, acl_id) do
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> send_resp(404, "ACL not found")
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
acl ->
|
||||||
|
cond do
|
||||||
|
is_nil(acl.api_key) ->
|
||||||
|
conn
|
||||||
|
|> send_resp(401, "Unauthorized (no API key set for ACL)")
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
acl.api_key == incoming_token ->
|
||||||
|
conn
|
||||||
|
|
||||||
|
true ->
|
||||||
|
conn
|
||||||
|
|> send_resp(401, "Unauthorized (invalid API key for ACL)")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> send_resp(400, "ACL ID not provided")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> send_resp(401, "Missing or invalid 'Bearer' token")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
defmodule WandererAppWeb.Plugs.CheckCharacterApiDisabled do
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
def init(opts), do: opts
|
||||||
|
|
||||||
|
def call(conn, _opts) do
|
||||||
|
if WandererApp.Env.character_api_disabled?() do
|
||||||
|
conn
|
||||||
|
|> send_resp(403, "Character API is disabled")
|
||||||
|
|> halt()
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
alias WandererAppWeb.UtilAPIController, as: Util
|
||||||
|
|
||||||
def init(opts), do: opts
|
def init(opts), do: opts
|
||||||
|
|
||||||
@@ -37,10 +38,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_map(query_params) do
|
defp fetch_map(query_params) do
|
||||||
case fetch_map_id(query_params) do
|
case Util.fetch_map_id(query_params) do
|
||||||
{:ok, {:map, map}} ->
|
|
||||||
{:ok, map}
|
|
||||||
|
|
||||||
{:ok, map_id} ->
|
{:ok, map_id} ->
|
||||||
WandererApp.Api.Map.by_id(map_id)
|
WandererApp.Api.Map.by_id(map_id)
|
||||||
|
|
||||||
@@ -48,20 +46,4 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
|||||||
error
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
|
|
||||||
{:ok, mid}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
|
|
||||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
|
||||||
{:ok, map} ->
|
|
||||||
{:ok, {:map, map}}
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
|
||||||
{:error, "No map found for slug=#{slug}"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -314,6 +314,24 @@ defmodule WandererAppWeb.AccessListsLive do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("generate-api-key", _params, socket) do
|
||||||
|
new_api_key = UUID.uuid4()
|
||||||
|
new_params = Map.put(socket.assigns.form.params || %{}, "api_key", new_api_key)
|
||||||
|
form = AshPhoenix.Form.validate(socket.assigns.form, new_params)
|
||||||
|
{:noreply, assign(socket, form: form)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("noop", _, socket) do
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event(event, body, socket) do
|
||||||
|
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(
|
def handle_info(
|
||||||
{"update_role", %{member_id: member_id, role: role}},
|
{"update_role", %{member_id: member_id, role: role}},
|
||||||
@@ -328,17 +346,6 @@ defmodule WandererAppWeb.AccessListsLive do
|
|||||||
{:noreply, socket |> maybe_update_role(member, role_atom, access_list)}
|
{:noreply, socket |> maybe_update_role(member, role_atom, access_list)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("noop", _, socket) do
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event(event, body, socket) do
|
|
||||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:search, text}, socket) do
|
def handle_info({:search, text}, socket) do
|
||||||
active_character_id =
|
active_character_id =
|
||||||
|
|||||||
@@ -142,6 +142,50 @@
|
|||||||
placeholder="Select an owner"
|
placeholder="Select an owner"
|
||||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Divider between above inputs and the API key section -->
|
||||||
|
<hr class="my-4 border-gray-600" />
|
||||||
|
|
||||||
|
<!-- API Key Section with grid layout -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||||
|
<div class="grid grid-cols-12 gap-2">
|
||||||
|
<div class="col-span-7">
|
||||||
|
<.input
|
||||||
|
type="text"
|
||||||
|
field={f[:api_key]}
|
||||||
|
placeholder="No API Key yet"
|
||||||
|
readonly
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-click="generate-api-key"
|
||||||
|
class="p-button p-component p-button-primary w-full"
|
||||||
|
style="min-width: 0;"
|
||||||
|
>
|
||||||
|
<span class="p-button-label">Generate</span>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-hook="CopyToClipboard"
|
||||||
|
id="copy-acl-api-key"
|
||||||
|
data-url={f[:api_key].value}
|
||||||
|
disabled={is_nil(f[:api_key].value) or f[:api_key].value == ""}
|
||||||
|
class={"p-button p-component w-full " <> if(is_nil(f[:api_key].value) or f[:api_key].value == "", do: "p-disabled", else: "")}
|
||||||
|
>
|
||||||
|
<span class="p-button-label">Copy</span>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4 border-gray-600" />
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||||
<%= (@live_action == :create && "Create") || "Save" %>
|
<%= (@live_action == :create && "Create") || "Save" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="card h-[250px] rounded-none bg-gradient-to-l from-stone-950 to-stone-900 hover:text-white transform transition duration-500"
|
class="card h-[250px] rounded-none bg-gradient-to-l from-stone-950 to-stone-900 hover:text-white transform transition duration-500"
|
||||||
patch={~p"/maps/new"}
|
patch={~p"/maps/new"}
|
||||||
>
|
>
|
||||||
<div class="card-body justify-center items-center ">
|
<div class="card-body justify-center items-center">
|
||||||
<.icon name="hero-plus-solid" class="w-20 h-20" />
|
<.icon name="hero-plus-solid" class="w-20 h-20" />
|
||||||
<h3 class="card-title text-center text-md">Create Map</h3>
|
<h3 class="card-title text-center text-md">Create Map</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,6 +125,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.modal
|
<.modal
|
||||||
:if={@is_connected? && @live_action in [:create, :edit]}
|
:if={@is_connected? && @live_action in [:create, :edit]}
|
||||||
title={"#{(@live_action == :create && "Create") || "Edit"} Map"}
|
title={"#{(@live_action == :create && "Create") || "Edit"} Map"}
|
||||||
@@ -256,9 +257,7 @@
|
|||||||
:if={@map_subscriptions_enabled?}
|
:if={@map_subscriptions_enabled?}
|
||||||
class={[
|
class={[
|
||||||
"p-unselectable-text",
|
"p-unselectable-text",
|
||||||
classes(
|
classes("p-tabview-selected p-highlight": @active_settings_tab == "subscription")
|
||||||
"p-tabview-selected p-highlight": @active_settings_tab == "subscription"
|
|
||||||
)
|
|
||||||
]}
|
]}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
data-pc-name=""
|
data-pc-name=""
|
||||||
@@ -310,9 +309,7 @@
|
|||||||
:if={not WandererApp.Env.public_api_disabled?()}
|
:if={not WandererApp.Env.public_api_disabled?()}
|
||||||
class={[
|
class={[
|
||||||
"p-unselectable-text",
|
"p-unselectable-text",
|
||||||
classes(
|
classes("p-tabview-selected p-highlight": @active_settings_tab == "public_api")
|
||||||
"p-tabview-selected p-highlight": @active_settings_tab == "public_api"
|
|
||||||
)
|
|
||||||
]}
|
]}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
data-pc-name=""
|
data-pc-name=""
|
||||||
@@ -414,10 +411,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={
|
:if={@active_settings_tab == "public_api" and not WandererApp.Env.public_api_disabled?()}
|
||||||
@active_settings_tab == "public_api" and
|
|
||||||
not WandererApp.Env.public_api_disabled?()
|
|
||||||
}
|
|
||||||
class="p-6"
|
class="p-6"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold mb-4">Public API</h2>
|
<h2 class="text-lg font-semibold mb-4">Public API</h2>
|
||||||
@@ -439,29 +433,28 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<.button class="btn btn-primary rounded-md" phx-click="generate-map-api-key">
|
<.button
|
||||||
Generate
|
type="button"
|
||||||
|
phx-click="generate-map-api-key"
|
||||||
|
class="p-button p-component p-button-primary"
|
||||||
|
style="min-width: 120px;"
|
||||||
|
>
|
||||||
|
<span class="p-button-label">Generate</span>
|
||||||
</.button>
|
</.button>
|
||||||
<.button
|
<.button
|
||||||
|
type="button"
|
||||||
phx-hook="CopyToClipboard"
|
phx-hook="CopyToClipboard"
|
||||||
id="copy-map-api-key"
|
id="copy-map-api-key"
|
||||||
data-url={@public_api_key}
|
data-url={@public_api_key}
|
||||||
disabled={is_nil(@public_api_key)}
|
disabled={is_nil(@public_api_key)}
|
||||||
class={
|
class={"p-button p-component " <> if(is_nil(@public_api_key), do: "p-disabled", else: "")}
|
||||||
if is_nil(@public_api_key) do
|
|
||||||
"copy-link btn rounded-md transition-colors duration-300
|
|
||||||
bg-gray-500 hover:bg-gray-500 text-gray-300 cursor-not-allowed"
|
|
||||||
else
|
|
||||||
"copy-link btn rounded-md transition-colors duration-300
|
|
||||||
bg-blue-600 hover:bg-blue-700 text-white cursor-pointer"
|
|
||||||
end
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Copy
|
<span class="p-button-label">Copy</span>
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :if={@active_settings_tab == "balance"}>
|
<div :if={@active_settings_tab == "balance"}>
|
||||||
<div class="stats w-full bg-primary text-primary-content">
|
<div class="stats w-full bg-primary text-primary-content">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
@@ -687,10 +680,8 @@
|
|||||||
<.button
|
<.button
|
||||||
:if={@active_settings_tab == "subscription" && not @is_adding_subscription?}
|
:if={@active_settings_tab == "subscription" && not @is_adding_subscription?}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={
|
disabled={@map_subscriptions |> Enum.at(0) |> Map.get(:status) == :active &&
|
||||||
@map_subscriptions |> Enum.at(0) |> Map.get(:status) == :active &&
|
@map_subscriptions |> Enum.at(0) |> Map.get(:plan) != :alpha}
|
||||||
@map_subscriptions |> Enum.at(0) |> Map.get(:plan) != :alpha
|
|
||||||
}
|
|
||||||
phx-click="add_subscription"
|
phx-click="add_subscription"
|
||||||
>
|
>
|
||||||
Add subscription
|
Add subscription
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule WandererAppWeb.Router do
|
|||||||
use Plug.ErrorHandler
|
use Plug.ErrorHandler
|
||||||
|
|
||||||
import PlugDynamic.Builder
|
import PlugDynamic.Builder
|
||||||
|
import Logger
|
||||||
|
|
||||||
import WandererAppWeb.UserAuth,
|
import WandererAppWeb.UserAuth,
|
||||||
warn: false,
|
warn: false,
|
||||||
@@ -118,6 +119,14 @@ defmodule WandererAppWeb.Router do
|
|||||||
plug WandererAppWeb.Plugs.CheckApiDisabled
|
plug WandererAppWeb.Plugs.CheckApiDisabled
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :api_character do
|
||||||
|
plug WandererAppWeb.Plugs.CheckCharacterApiDisabled
|
||||||
|
end
|
||||||
|
|
||||||
|
pipeline :api_acl do
|
||||||
|
plug WandererAppWeb.Plugs.CheckAclApiKey
|
||||||
|
end
|
||||||
|
|
||||||
scope "/api/map/systems-kills", WandererAppWeb do
|
scope "/api/map/systems-kills", WandererAppWeb do
|
||||||
pipe_through [:api, :api_map, :api_kills]
|
pipe_through [:api, :api_map, :api_kills]
|
||||||
|
|
||||||
@@ -126,66 +135,77 @@ defmodule WandererAppWeb.Router do
|
|||||||
|
|
||||||
scope "/api/map", WandererAppWeb do
|
scope "/api/map", WandererAppWeb do
|
||||||
pipe_through [:api, :api_map]
|
pipe_through [:api, :api_map]
|
||||||
|
|
||||||
# GET /api/map/systems?map_id=... or ?slug=...
|
|
||||||
get "/systems", MapAPIController, :list_systems
|
get "/systems", MapAPIController, :list_systems
|
||||||
|
|
||||||
# GET /api/map/system?id=... plus either map_id=... or slug=...
|
|
||||||
get "/system", MapAPIController, :show_system
|
get "/system", MapAPIController, :show_system
|
||||||
|
|
||||||
# GET /api/map/characters?map_id=... or slug=...
|
|
||||||
get "/characters", MapAPIController, :tracked_characters_with_info
|
get "/characters", MapAPIController, :tracked_characters_with_info
|
||||||
|
|
||||||
# GET /api/map/structure-timers?map_id=... or slug=... and optionally ?system_id=...
|
|
||||||
get "/structure-timers", MapAPIController, :show_structure_timers
|
get "/structure-timers", MapAPIController, :show_structure_timers
|
||||||
|
get "/acls", MapAccessListAPIController, :index
|
||||||
|
post "/acls", MapAccessListAPIController, :create
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
scope "/api/characters", WandererAppWeb do
|
||||||
|
pipe_through [:api, :api_character]
|
||||||
|
get "/", CharactersAPIController, :index
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/api/acls", WandererAppWeb do
|
||||||
|
pipe_through [:api, :api_acl]
|
||||||
|
|
||||||
|
get "/:id", MapAccessListAPIController, :show
|
||||||
|
put "/:id", MapAccessListAPIController, :update
|
||||||
|
post "/:acl_id/members", AccessListMemberAPIController, :create
|
||||||
|
put "/:acl_id/members/:member_id", AccessListMemberAPIController, :update_role
|
||||||
|
delete "/:acl_id/members/:member_id", AccessListMemberAPIController, :delete
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/common", WandererAppWeb do
|
scope "/api/common", WandererAppWeb do
|
||||||
pipe_through [:api]
|
pipe_through [:api]
|
||||||
|
|
||||||
# GET /api/common/system-static-info?id=...
|
|
||||||
get "/system-static-info", CommonAPIController, :show_system_static
|
get "/system-static-info", CommonAPIController, :show_system_static
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Browser / blog stuff
|
||||||
|
#
|
||||||
scope "/", WandererAppWeb do
|
scope "/", WandererAppWeb do
|
||||||
pipe_through [:browser, :blog, :redirect_if_user_is_authenticated]
|
pipe_through [:browser, :blog, :redirect_if_user_is_authenticated]
|
||||||
|
|
||||||
get "/welcome", BlogController, :index
|
get "/welcome", BlogController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/contacts", WandererAppWeb do
|
scope "/contacts", WandererAppWeb do
|
||||||
pipe_through [:browser, :blog]
|
pipe_through [:browser, :blog]
|
||||||
|
|
||||||
get "/", BlogController, :contacts
|
get "/", BlogController, :contacts
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/changelog", WandererAppWeb do
|
scope "/changelog", WandererAppWeb do
|
||||||
pipe_through [:browser, :blog]
|
pipe_through [:browser, :blog]
|
||||||
|
|
||||||
get "/", BlogController, :changelog
|
get "/", BlogController, :changelog
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/news", WandererAppWeb do
|
scope "/news", WandererAppWeb do
|
||||||
pipe_through [:browser, :blog]
|
pipe_through [:browser, :blog]
|
||||||
|
|
||||||
get "/:slug", BlogController, :show
|
get "/:slug", BlogController, :show
|
||||||
get "/", BlogController, :list
|
get "/", BlogController, :list
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/license", WandererAppWeb do
|
scope "/license", WandererAppWeb do
|
||||||
pipe_through [:browser, :blog]
|
pipe_through [:browser, :blog]
|
||||||
|
|
||||||
get "/", BlogController, :license
|
get "/", BlogController, :license
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Auth
|
||||||
|
#
|
||||||
scope "/auth", WandererAppWeb do
|
scope "/auth", WandererAppWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/signout", AuthController, :signout
|
get "/signout", AuthController, :signout
|
||||||
get "/:provider", AuthController, :request
|
get "/:provider", AuthController, :request
|
||||||
get "/:provider/callback", AuthController, :callback
|
get "/:provider/callback", AuthController, :callback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Admin
|
||||||
|
#
|
||||||
scope "/admin", WandererAppWeb do
|
scope "/admin", WandererAppWeb do
|
||||||
pipe_through(:browser)
|
pipe_through(:browser)
|
||||||
pipe_through(:admin_bauth)
|
pipe_through(:admin_bauth)
|
||||||
@@ -207,53 +227,49 @@ defmodule WandererAppWeb.Router do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Additional routes / Live sessions
|
||||||
|
#
|
||||||
scope "/", WandererAppWeb do
|
scope "/", WandererAppWeb do
|
||||||
pipe_through(:browser)
|
pipe_through(:browser)
|
||||||
|
|
||||||
get "/", RedirectController, :redirect_authenticated
|
get "/", RedirectController, :redirect_authenticated
|
||||||
get("/last", MapsController, :last)
|
get "/last", MapsController, :last
|
||||||
|
|
||||||
live_session :authenticated,
|
live_session :authenticated,
|
||||||
on_mount: [
|
on_mount: [
|
||||||
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
||||||
WandererAppWeb.Nav
|
WandererAppWeb.Nav
|
||||||
] do
|
] do
|
||||||
live("/access-lists/new", AccessListsLive, :create)
|
live "/access-lists/new", AccessListsLive, :create
|
||||||
live("/access-lists/:id/edit", AccessListsLive, :edit)
|
live "/access-lists/:id/edit", AccessListsLive, :edit
|
||||||
live("/access-lists/:id/add-members", AccessListsLive, :add_members)
|
live "/access-lists/:id/add-members", AccessListsLive, :add_members
|
||||||
live("/access-lists/:id", AccessListsLive, :members)
|
live "/access-lists/:id", AccessListsLive, :members
|
||||||
live("/access-lists", AccessListsLive, :index)
|
live "/access-lists", AccessListsLive, :index
|
||||||
live("/coming-soon", ComingLive, :index)
|
|
||||||
live("/tracking/:slug", CharactersTrackingLive, :characters)
|
live "/coming-soon", ComingLive, :index
|
||||||
live("/tracking", CharactersTrackingLive, :index)
|
live "/tracking/:slug", CharactersTrackingLive, :characters
|
||||||
live("/characters", CharactersLive, :index)
|
live "/tracking", CharactersTrackingLive, :index
|
||||||
live("/characters/authorize", CharactersLive, :authorize)
|
live "/characters", CharactersLive, :index
|
||||||
live("/maps/new", MapsLive, :create)
|
live "/characters/authorize", CharactersLive, :authorize
|
||||||
live("/maps/:slug/edit", MapsLive, :edit)
|
live "/maps/new", MapsLive, :create
|
||||||
live("/maps/:slug/settings", MapsLive, :settings)
|
live "/maps/:slug/edit", MapsLive, :edit
|
||||||
live("/maps", MapsLive, :index)
|
live "/maps/:slug/settings", MapsLive, :settings
|
||||||
live("/profile", ProfileLive, :index)
|
live "/maps", MapsLive, :index
|
||||||
live("/profile/deposit", ProfileLive, :deposit)
|
live "/profile", ProfileLive, :index
|
||||||
live("/profile/subscribe", ProfileLive, :subscribe)
|
live "/profile/deposit", ProfileLive, :deposit
|
||||||
live("/:slug/audit", MapAuditLive, :index)
|
live "/profile/subscribe", ProfileLive, :subscribe
|
||||||
live("/:slug", MapLive, :index)
|
live "/:slug/audit", MapAuditLive, :index
|
||||||
|
live "/:slug", MapLive, :index
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
|
||||||
if Application.compile_env(:wanderer_app, :dev_routes) do
|
if Application.compile_env(:wanderer_app, :dev_routes) do
|
||||||
# If you want to use the LiveDashboard in production, you should put
|
|
||||||
# it behind authentication and allow only admins to access it.
|
|
||||||
# If your application does not have an admins-only section yet,
|
|
||||||
# you can use Plug.BasicAuth to set up some basic authentication
|
|
||||||
# as long as you are also using SSL (which you should anyway).
|
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
scope "/dev" do
|
scope "/dev" do
|
||||||
pipe_through(:browser)
|
pipe_through(:browser)
|
||||||
|
|
||||||
error_tracker_dashboard("/errors", as: :error_tracker_dev_dashboard)
|
error_tracker_dashboard("/errors", as: :error_tracker_dev_dashboard)
|
||||||
|
|
||||||
live_dashboard("/dashboard", metrics: WandererAppWeb.Telemetry)
|
live_dashboard("/dashboard", metrics: WandererAppWeb.Telemetry)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
425
priv/posts/2025/02-20-acl-api.md
Normal file
425
priv/posts/2025/02-20-acl-api.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
%{
|
||||||
|
title: "User Guide: Characters & ACL API Endpoints",
|
||||||
|
author: "Wanderer Team",
|
||||||
|
cover_image_uri: "/images/news/02-20-acl-api/generate-key.png",
|
||||||
|
tags: ~w(acl characters guide interface),
|
||||||
|
description: "Learn how to retrieve and manage Access Lists and Characters through the Wanderer public APIs. This guide covers available endpoints, request examples, and sample responses."
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Wanderer’s expanded public API now lets you retrieve **all characters** in the system and manage “Access Lists” (ACLs) for controlling visibility or permissions. These endpoints allow you to:
|
||||||
|
|
||||||
|
- Fetch a list of **all** EVE characters known to the system.
|
||||||
|
- List ACLs for a given map.
|
||||||
|
- Create new ACLs for maps (with automatic API key generation).
|
||||||
|
- Update existing ACLs.
|
||||||
|
- Add, remove, and change the roles of ACL members.
|
||||||
|
|
||||||
|
This guide provides step-by-step instructions, request/response examples, and details on how to authenticate each call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Unless otherwise noted, these endpoints require a valid **Bearer** token. Pass it in the `Authorization` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Authorization: Bearer <REDACTED_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the token is missing or invalid, you’ll receive a `401 Unauthorized` error.
|
||||||
|
_(No API key is required for some “common” endpoints, but ACL- and character-related endpoints require a valid token.)_
|
||||||
|
|
||||||
|
There are two types of tokens in use:
|
||||||
|
|
||||||
|
1. **Map API Token:** Available in the map settings. This token is used for map-specific endpoints (e.g. listing ACLs for a map and creating ACLs).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. **ACL API Token:** Available in the create/edit ACL screen. This token is used for ACL member management endpoints.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints Overview
|
||||||
|
|
||||||
|
### 1. List **All** Characters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/characters
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Returns a list of **all** characters known to Wanderer.
|
||||||
|
- **Toggle:** Controlled by the environment variable `WANDERER_CHARACTER_API_DISABLED` (default is `false`).
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/characters"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "b374d9e6-47a7-4e20-85ad-d608809827b5",
|
||||||
|
"name": "Some Character",
|
||||||
|
"eve_id": "2122825111",
|
||||||
|
"corporation_name": "School of Applied Knowledge",
|
||||||
|
"alliance_name": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6963bee6-eaa1-40e2-8200-4bc2fcbd7350",
|
||||||
|
"name": "Other Character",
|
||||||
|
"eve_id": "2122019111",
|
||||||
|
"corporation_name": "Some Corporation",
|
||||||
|
"alliance_name": null
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `eve_id` when referencing a character in ACL operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. List ACLs for a Given Map
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/map/acls?map_id=<UUID>
|
||||||
|
GET /api/map/acls?slug=<map-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Lists all ACLs associated with a map, specified by either `map_id` (UUID) or `slug`.
|
||||||
|
- **Authentication:** Requires the Map API Token (available in map settings).
|
||||||
|
- **Example Request (using slug):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/map/acls?slug=mapname"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "19712899-ec3a-47b1-b73b-2bae221c5513",
|
||||||
|
"name": "aclName",
|
||||||
|
"description": null,
|
||||||
|
"owner_eve_id": "11111111111",
|
||||||
|
"inserted_at": "2025-02-13T03:32:25.144403Z",
|
||||||
|
"updated_at": "2025-02-13T03:32:25.144403Z",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Show a Specific ACL (Including Members)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET /api/acls/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Fetches a single ACL by ID, with its members preloaded.
|
||||||
|
- **Authentication:** Requires the ACL API Token.
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/acls/19712899-ec3a-47b1-b73b-2bae221c5513"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "19712899-ec3a-47b1-b73b-2bae221c5513",
|
||||||
|
"name": "aclName",
|
||||||
|
"description": null,
|
||||||
|
"owner_id": "d43a9083-2705-40c9-a314-f7f412346661",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": "8d63ab1e-b44f-4e81-8227-8fb8d928dad8",
|
||||||
|
"name": "Other Character",
|
||||||
|
"role": "admin",
|
||||||
|
"inserted_at": "2025-02-13T03:33:32.332598Z",
|
||||||
|
"updated_at": "2025-02-13T03:33:36.644520Z"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Create a New ACL Associated with a Map
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/map/acls
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Creates a new ACL for a map and generates a new ACL API key. The map record tracks its ACLs.
|
||||||
|
- **Required Query Parameter:** Either `map_id` (UUID) or `slug` (map slug).
|
||||||
|
- **Request Body Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"acl": {
|
||||||
|
"name": "New ACL",
|
||||||
|
"description": "Optional description",
|
||||||
|
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `owner_eve_id` must be the external EVE id (the `eve_id` from `/api/characters`).
|
||||||
|
- **Example Request (using map slug):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <MAP_API_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"acl": {
|
||||||
|
"name": "New ACL",
|
||||||
|
"description": "Optional description",
|
||||||
|
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/map/acls?slug=mapname"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Request (using map UUID):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <MAP_API_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"acl": {
|
||||||
|
"name": "New ACL",
|
||||||
|
"description": "Optional description",
|
||||||
|
"owner_eve_id": "EXTERNAL_EVE_ID"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/map/acls?map_id=YOUR_MAP_UUID"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "NEW_ACL_UUID",
|
||||||
|
"name": "New ACL",
|
||||||
|
"description": "Optional description",
|
||||||
|
"owner_id": "OWNER_ID",
|
||||||
|
"api_key": "GENERATED_ACL_API_KEY",
|
||||||
|
"inserted_at": "2025-02-14T17:00:00Z",
|
||||||
|
"updated_at": "2025-02-14T17:00:00Z",
|
||||||
|
"members": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Update an ACL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PUT /api/acls/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Updates an existing ACL (e.g. name, description, api_key).
|
||||||
|
The update endpoint fetches the ACL record first and then applies the update.
|
||||||
|
- **Authentication:** Requires the ACL API Token.
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT \
|
||||||
|
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"acl": {
|
||||||
|
"name": "Updated ACL Name",
|
||||||
|
"description": "This is the updated description",
|
||||||
|
"api_key": "EXISTING_ACL_API_KEY"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/acls/ACL_UUID"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "ACL_UUID",
|
||||||
|
"name": "Updated ACL Name",
|
||||||
|
"description": "This is the updated description",
|
||||||
|
"owner_id": "OWNER_ID",
|
||||||
|
"api_key": "EXISTING_ACL_API_KEY",
|
||||||
|
"inserted_at": "2025-02-14T16:49:13.423556Z",
|
||||||
|
"updated_at": "2025-02-14T17:22:51.343784Z",
|
||||||
|
"members": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Add a Member to an ACL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST /api/acls/:acl_id/members
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Adds a new member (character, corporation, or alliance) to the specified ACL.
|
||||||
|
- **Authentication:** Requires the ACL API Token.
|
||||||
|
- **Request Body Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"member": {
|
||||||
|
"name": "New Member",
|
||||||
|
"eve_character_id": "EXTERNAL_EVE_ID",
|
||||||
|
"role": "viewer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `eve_character_id` is the character’s external EVE id.
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"member": {
|
||||||
|
"name": "New Member",
|
||||||
|
"eve_character_id": "EXTERNAL_EVE_ID",
|
||||||
|
"role": "viewer"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/acls/ACL_UUID/members"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "MEMBERSHIP_UUID",
|
||||||
|
"name": "New Member",
|
||||||
|
"role": "viewer",
|
||||||
|
"inserted_at": "...",
|
||||||
|
"updated_at": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Change a Member’s Role
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PUT /api/acls/:acl_id/members/:member_id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Updates an ACL member’s role (e.g. from `viewer` to `admin`).
|
||||||
|
The `:member_id` is the external EVE id of the character.
|
||||||
|
- **Authentication:** Requires the ACL API Token.
|
||||||
|
- **Request Body Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"member": {
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT \
|
||||||
|
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"member": {
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
"https://wanderer.example.com/api/acls/ACL_UUID/members/EXTERNAL_EVE_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response (redacted):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "MEMBERSHIP_UUID",
|
||||||
|
"name": "New Member",
|
||||||
|
"role": "admin",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Remove a Member from an ACL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE /api/acls/:acl_id/members/:member_id
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Description:** Removes the member with the specified external EVE id from the ACL.
|
||||||
|
- **Authentication:** Requires the ACL API Token.
|
||||||
|
- **Example Request:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE \
|
||||||
|
-H "Authorization: Bearer <ACL_API_TOKEN>" \
|
||||||
|
"https://wanderer.example.com/api/acls/ACL_UUID/members/EXTERNAL_EVE_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Example Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This guide outlines how to:
|
||||||
|
|
||||||
|
1. **List** all characters (`GET /api/characters`) so you can pick a valid character to add to your ACL.
|
||||||
|
2. **List** ACLs for a specified map (`GET /api/map/acls?map_id=<UUID>` or `?slug=<map-slug>`).
|
||||||
|
3. **Show** ACL details, including its members (`GET /api/acls/:id`).
|
||||||
|
4. **Create** a new ACL for a map (`POST /api/map/acls`), which generates a new ACL API key.
|
||||||
|
5. **Update** an existing ACL (`PUT /api/acls/:id`).
|
||||||
|
6. **Add** members (characters, corporations, alliances) to an ACL (`POST /api/acls/:acl_id/members`).
|
||||||
|
7. **Change** a member’s role (`PUT /api/acls/:acl_id/members/:member_id`).
|
||||||
|
8. **Remove** a member from an ACL (`DELETE /api/acls/:acl_id/members/:member_id`).
|
||||||
|
|
||||||
|
By following these request patterns, you can manage your ACL resources in a fully programmatic fashion. If you have any questions, feel free to reach out to the Wanderer Team.
|
||||||
|
|
||||||
|
Fly safe,
|
||||||
|
**WANDERER TEAM**
|
||||||
21
priv/repo/migrations/20250213182400_add_acl_api_key.exs
Normal file
21
priv/repo/migrations/20250213182400_add_acl_api_key.exs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
defmodule WandererApp.Repo.Migrations.AddAclApiKey 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(:access_lists_v1) do
|
||||||
|
add :api_key, :text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:access_lists_v1) do
|
||||||
|
remove :api_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
108
priv/resource_snapshots/repo/access_lists_v1/20250213182400.json
Normal file
108
priv/resource_snapshots/repo/access_lists_v1/20250213182400.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"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?": 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": "api_key",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "inserted_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": false,
|
||||||
|
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "updated_at",
|
||||||
|
"type": "utc_datetime_usec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": {
|
||||||
|
"deferrable": false,
|
||||||
|
"destination_attribute": "id",
|
||||||
|
"destination_attribute_default": null,
|
||||||
|
"destination_attribute_generated": null,
|
||||||
|
"index?": false,
|
||||||
|
"match_type": null,
|
||||||
|
"match_with": null,
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"name": "access_lists_v1_owner_id_fkey",
|
||||||
|
"on_delete": null,
|
||||||
|
"on_update": null,
|
||||||
|
"primary_key?": true,
|
||||||
|
"schema": "public",
|
||||||
|
"table": "character_v1"
|
||||||
|
},
|
||||||
|
"size": null,
|
||||||
|
"source": "owner_id",
|
||||||
|
"type": "uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base_filter": null,
|
||||||
|
"check_constraints": [],
|
||||||
|
"custom_indexes": [],
|
||||||
|
"custom_statements": [],
|
||||||
|
"has_create_action": true,
|
||||||
|
"hash": "5118AF0DEBEEED63DC30565ECFFEDF682876FAD476AF2796E973C6883E4054E0",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.WandererApp.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "access_lists_v1"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user