diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f77352d2..77f7963a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -116,7 +116,6 @@ jobs: matrix: platform: - linux/amd64 - - linux/arm64 steps: - name: Prepare run: | diff --git a/config/runtime.exs b/config/runtime.exs index 7b050911..192b8930 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -119,6 +119,7 @@ config :wanderer_app, corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""), corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"), public_api_disabled: public_api_disabled, + active_tracking_pool: System.get_env("WANDERER_ACTIVE_TRACKING_POOL", "default"), character_tracking_pause_disabled: System.get_env("WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED", "true") |> String.to_existing_atom(), @@ -174,11 +175,31 @@ config :ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth, client_id: {WandererApp.Ueberauth, :client_id}, client_secret: {WandererApp.Ueberauth, :client_secret}, client_id_default: System.get_env("EVE_CLIENT_ID", ""), + client_id_1: System.get_env("EVE_CLIENT_ID_1", ""), + client_id_2: System.get_env("EVE_CLIENT_ID_2", ""), + client_id_3: System.get_env("EVE_CLIENT_ID_3", ""), + client_id_4: System.get_env("EVE_CLIENT_ID_4", ""), + client_id_5: System.get_env("EVE_CLIENT_ID_5", ""), + client_id_6: System.get_env("EVE_CLIENT_ID_6", ""), + client_id_7: System.get_env("EVE_CLIENT_ID_7", ""), + client_id_8: System.get_env("EVE_CLIENT_ID_8", ""), + client_id_9: System.get_env("EVE_CLIENT_ID_9", ""), + client_id_10: System.get_env("EVE_CLIENT_ID_10", ""), client_id_with_wallet: System.get_env("EVE_CLIENT_WITH_WALLET_ID", ""), client_id_with_corp_wallet: System.get_env("EVE_CLIENT_WITH_CORP_WALLET_ID", ""), client_secret_default: System.get_env("EVE_CLIENT_SECRET", ""), + client_secret_1: System.get_env("EVE_CLIENT_SECRET_1", ""), + client_secret_2: System.get_env("EVE_CLIENT_SECRET_2", ""), + client_secret_3: System.get_env("EVE_CLIENT_SECRET_3", ""), + client_secret_4: System.get_env("EVE_CLIENT_SECRET_4", ""), + client_secret_5: System.get_env("EVE_CLIENT_SECRET_5", ""), + client_secret_6: System.get_env("EVE_CLIENT_SECRET_6", ""), + client_secret_7: System.get_env("EVE_CLIENT_SECRET_7", ""), + client_secret_8: System.get_env("EVE_CLIENT_SECRET_8", ""), + client_secret_9: System.get_env("EVE_CLIENT_SECRET_9", ""), + client_secret_10: System.get_env("EVE_CLIENT_SECRET_10", ""), client_secret_with_wallet: System.get_env("EVE_CLIENT_WITH_WALLET_SECRET", ""), client_secret_with_corp_wallet: diff --git a/lib/wanderer_app/api/character.ex b/lib/wanderer_app/api/character.ex index 316dbb4e..6dd8cea8 100644 --- a/lib/wanderer_app/api/character.ex +++ b/lib/wanderer_app/api/character.ex @@ -24,6 +24,7 @@ defmodule WandererApp.Api.Character do define(:update_alliance, action: :update_alliance) define(:update_wallet_balance, action: :update_wallet_balance) define(:mark_as_deleted, action: :mark_as_deleted) + define(:last_active, action: :last_active) define(:by_id, get_by: [:id], @@ -47,7 +48,8 @@ defmodule WandererApp.Api.Character do :access_token, :refresh_token, :expires_at, - :scopes + :scopes, + :tracking_pool ] defaults [:create, :read, :destroy] @@ -72,6 +74,12 @@ defmodule WandererApp.Api.Character do filter(expr(user_id == ^arg(:user_id) and deleted == false)) end + read :last_active do + argument(:from, :utc_datetime, allow_nil?: false) + + filter(expr(updated_at > ^arg(:from))) + end + update :assign do accept [] require_atomic? false @@ -85,7 +93,7 @@ defmodule WandererApp.Api.Character do update :update do require_atomic? false - accept([:name, :access_token, :refresh_token, :expires_at, :scopes]) + accept([:name, :access_token, :refresh_token, :expires_at, :scopes, :tracking_pool]) change(set_attribute(:deleted, false)) end @@ -198,6 +206,7 @@ defmodule WandererApp.Api.Character do attribute :alliance_name, :string attribute :alliance_ticker, :string attribute :eve_wallet_balance, :float + attribute :tracking_pool, :string create_timestamp(:inserted_at) update_timestamp(:updated_at) diff --git a/lib/wanderer_app/application.ex b/lib/wanderer_app/application.ex index 43cff4f8..d2abb1fb 100644 --- a/lib/wanderer_app/application.ex +++ b/lib/wanderer_app/application.ex @@ -27,6 +27,7 @@ defmodule WandererApp.Application do } }, WandererApp.Cache, + Supervisor.child_spec({Cachex, name: :esi_auth_cache}, id: :esi_auth_cache_worker), Supervisor.child_spec({Cachex, name: :api_cache}, id: :api_cache_worker), Supervisor.child_spec({Cachex, name: :system_static_info_cache}, id: :system_static_info_cache_worker @@ -40,6 +41,7 @@ defmodule WandererApp.Application do Supervisor.child_spec({Cachex, name: :tracked_characters}, id: :tracked_characters_cache_worker ), + WandererApp.Esi.InitClientsTask, WandererApp.Scheduler, {Registry, keys: :unique, name: WandererApp.MapRegistry}, {Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry}, diff --git a/lib/wanderer_app/env.ex b/lib/wanderer_app/env.ex index be4e4efa..90f297bb 100644 --- a/lib/wanderer_app/env.ex +++ b/lib/wanderer_app/env.ex @@ -16,6 +16,12 @@ defmodule WandererApp.Env do def invites, do: get_key(:invites, false) def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false) def public_api_disabled?, do: get_key(:public_api_disabled, false) + + @decorate cacheable( + cache: WandererApp.Cache, + key: "active_tracking_pool" + ) + def active_tracking_pool, do: get_key(:active_tracking_pool, "default") def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true) def character_api_disabled?, do: get_key(:character_api_disabled, false) def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false) diff --git a/lib/wanderer_app/esi/api_client.ex b/lib/wanderer_app/esi/api_client.ex index 9e1be024..8a5043a3 100644 --- a/lib/wanderer_app/esi/api_client.ex +++ b/lib/wanderer_app/esi/api_client.ex @@ -671,13 +671,20 @@ defmodule WandererApp.Esi.ApiClient do end defp refresh_token(character_id) do - {:ok, %{expires_at: expires_at, refresh_token: refresh_token, scopes: scopes} = character} = + {:ok, + %{ + expires_at: expires_at, + refresh_token: refresh_token, + scopes: scopes, + tracking_pool: tracking_pool + } = character} = WandererApp.Character.get_character(character_id) refresh_token_result = WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([], with_wallet: WandererApp.Character.can_track_wallet?(character), is_admin?: WandererApp.Character.can_track_corp_wallet?(character), + tracking_pool: tracking_pool, token: %OAuth2.AccessToken{refresh_token: refresh_token} ) diff --git a/lib/wanderer_app/ueberauth/strategy/eve.ex b/lib/wanderer_app/ueberauth/strategy/eve.ex index 13ed916f..6658e808 100644 --- a/lib/wanderer_app/ueberauth/strategy/eve.ex +++ b/lib/wanderer_app/ueberauth/strategy/eve.ex @@ -192,10 +192,13 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do end defp oauth_client_options_from_conn(conn, with_wallet, is_admin?) do + tracking_pool = WandererApp.Env.active_tracking_pool() + base_options = [ redirect_uri: callback_url(conn), with_wallet: with_wallet, - is_admin?: is_admin? + is_admin?: is_admin?, + tracking_pool: tracking_pool ] request_options = conn.private[:ueberauth_request_options].options diff --git a/lib/wanderer_app/ueberauth/strategy/eve/init_configs_task.ex b/lib/wanderer_app/ueberauth/strategy/eve/init_configs_task.ex new file mode 100644 index 00000000..38e1a6d8 --- /dev/null +++ b/lib/wanderer_app/ueberauth/strategy/eve/init_configs_task.ex @@ -0,0 +1,77 @@ +defmodule WandererApp.Esi.InitClientsTask do + use Task + + require Logger + + def start_link(arg) do + Task.start_link(__MODULE__, :run, [arg]) + end + + def run(_arg) do + Logger.info("starting") + + Cachex.put( + :esi_auth_cache, + :active_config, + "config_#{WandererApp.Env.active_tracking_pool()}" + ) + + cache_clients() + end + + defp cache_clients() do + config = Application.get_env(:ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth, []) + + cache_client("default", %{ + client_id: config[:client_id_default], + client_secret: config[:client_secret_default] + }) + + Enum.each(1..10, fn index -> + client_id = config["client_id_#{index}" |> String.to_atom()] + client_secret = config["client_secret_#{index}" |> String.to_atom()] + + if client_id != "" && client_secret != "" do + cache_client(index, %{ + client_id: client_id, + client_secret: client_secret + }) + end + end) + end + + defp cache_client(id, config) do + config_uuid = UUID.uuid4() + + config = + config + |> Map.merge(%{ + id: id, + uuid: config_uuid + }) + + Cachex.put( + :esi_auth_cache, + "config_#{id}", + config + ) + + Cachex.put( + :esi_auth_cache, + config_uuid, + config + ) + + # Cachex.put( + # :esi_auth_cache, + # "config_uuid_#{id}", + # config_uuid + # ) + + Cachex.put( + :esi_auth_cache, + "configs_total_count", + id + ) + end +end diff --git a/lib/wanderer_app/ueberauth/uberauth.ex b/lib/wanderer_app/ueberauth/uberauth.ex index a0ac6542..4c59f368 100644 --- a/lib/wanderer_app/ueberauth/uberauth.ex +++ b/lib/wanderer_app/ueberauth/uberauth.ex @@ -2,26 +2,50 @@ defmodule WandererApp.Ueberauth do @moduledoc false def client_id(opts \\ []) do - config = _get_config() + config = get_config() + tracking_pool = Keyword.get(opts, :tracking_pool) cond do Keyword.get(opts, :is_admin?) -> config[:client_id_with_corp_wallet] Keyword.get(opts, :with_wallet) -> config[:client_id_with_wallet] + not is_nil(tracking_pool) -> get_settings(tracking_pool)[:client_id] true -> config[:client_id_default] end end def client_secret(opts \\ []) do - config = _get_config() + config = get_config() + tracking_pool = Keyword.get(opts, :tracking_pool) cond do Keyword.get(opts, :is_admin?) -> config[:client_secret_with_corp_wallet] Keyword.get(opts, :with_wallet) -> config[:client_secret_with_wallet] + not is_nil(tracking_pool) -> get_settings(tracking_pool)[:client_secret] true -> config[:client_secret_default] end end - defp _get_config() do + defp get_settings(nil) do + {:ok, esi_config} = + Cachex.get( + :esi_auth_cache, + "config_default" + ) + + esi_config + end + + defp get_settings(tracking_pool) do + {:ok, esi_config} = + Cachex.get( + :esi_auth_cache, + "config_#{tracking_pool}" + ) + + esi_config + end + + defp get_config() do Application.get_env(:ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth, []) end end diff --git a/lib/wanderer_app_web/controllers/auth_controller.ex b/lib/wanderer_app_web/controllers/auth_controller.ex index b43e06ff..23cf1de4 100644 --- a/lib/wanderer_app_web/controllers/auth_controller.ex +++ b/lib/wanderer_app_web/controllers/auth_controller.ex @@ -14,7 +14,8 @@ defmodule WandererAppWeb.AuthController do access_token: auth.credentials.token, refresh_token: auth.credentials.refresh_token, expires_at: auth.credentials.expires_at, - scopes: auth.credentials.scopes + scopes: auth.credentials.scopes, + tracking_pool: WandererApp.Env.active_tracking_pool() } %{ @@ -29,7 +30,8 @@ defmodule WandererAppWeb.AuthController do access_token: auth.credentials.token, refresh_token: auth.credentials.refresh_token, expires_at: auth.credentials.expires_at, - scopes: auth.credentials.scopes + scopes: auth.credentials.scopes, + tracking_pool: WandererApp.Env.active_tracking_pool() } {:ok, character} = diff --git a/lib/wanderer_app_web/live/admin/admin_live.ex b/lib/wanderer_app_web/live/admin/admin_live.ex index cc56a57b..18338493 100755 --- a/lib/wanderer_app_web/live/admin/admin_live.ex +++ b/lib/wanderer_app_web/live/admin/admin_live.ex @@ -62,6 +62,7 @@ defmodule WandererAppWeb.AdminLive do user_character_ids: user_character_ids, user_id: user_id, invite_link: nil, + tracker_stats: [], map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(), restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?() )} @@ -294,6 +295,8 @@ defmodule WandererAppWeb.AdminLive do defp apply_action(socket, :index, _params, uri) do {:ok, invites} = WandererApp.Api.MapInvite.read() + {:ok, tracker_stats} = load_tracker_stats() + socket |> assign(:active_page, :admin) |> assign(:uri, URI.parse(uri)) @@ -308,6 +311,7 @@ defmodule WandererAppWeb.AdminLive do |> assign(:form, to_form(%{"amount" => 500_000_000})) |> assign(:unlink_character_form, to_form(%{})) |> assign(:invites, invites) + |> assign(:tracker_stats, tracker_stats) end defp apply_action(socket, :add_invite_link, _params, uri) do @@ -344,6 +348,64 @@ defmodule WandererAppWeb.AdminLive do |> assign(:invites, []) end + defp load_tracker_stats() do + {:ok, characters} = + WandererApp.Api.Character.last_active(%{ + from: + DateTime.utc_now() + |> DateTime.add(-1 * 60 * 24 * 7, :minute) + }) + + admins_count = + characters |> Enum.filter(&WandererApp.Character.can_track_corp_wallet?/1) |> Enum.count() + + with_wallets_count = + characters + |> Enum.filter( + &(WandererApp.Character.can_track_wallet?(&1) and + not WandererApp.Character.can_track_corp_wallet?(&1)) + ) + |> Enum.count() + + default_count = + characters + |> Enum.filter( + &(is_nil(&1.tracking_pool) and not WandererApp.Character.can_track_wallet?(&1) and + not WandererApp.Character.can_track_corp_wallet?(&1)) + ) + |> Enum.count() + + result = [ + %{title: "Admins", value: admins_count}, + %{title: "With Wallet", value: with_wallets_count}, + %{title: "Default", value: default_count} + ] + + {:ok, pools_count} = + Cachex.get( + :esi_auth_cache, + "configs_total_count" + ) + + pools = + 1..pools_count + |> Enum.map(fn pool_id -> + pools_character_count = + characters + |> Enum.filter( + &(&1.tracking_pool == "#{pool_id}" and not WandererApp.Character.can_track_wallet?(&1) and + not WandererApp.Character.can_track_corp_wallet?(&1)) + ) + |> Enum.count() + + %{title: "Pool #{pool_id}", value: pools_character_count} + end) + + result = result ++ pools + + {:ok, result} + end + defp get_invite_link(uri, token) do uri |> Map.put(:path, "/auth/eve") diff --git a/lib/wanderer_app_web/live/admin/admin_live.html.heex b/lib/wanderer_app_web/live/admin/admin_live.html.heex index ae0e421e..12b8f6cf 100644 --- a/lib/wanderer_app_web/live/admin/admin_live.html.heex +++ b/lib/wanderer_app_web/live/admin/admin_live.html.heex @@ -121,50 +121,69 @@ <.icon name="hero-plus-solid" class="w-6 h-6" />

New Invite

+ + <.table + id="invites" + rows={@invites} + class="!max-h-[40vh] !overflow-y-auto" + > + <:col :let={invite} label="Link"> +
+
<%= get_invite_link(@uri, invite.token) %>
+ <.button + phx-hook="CopyToClipboard" + id="copy-to-clipboard" + class="copy-link btn btn-neutral rounded-none" + data-url={get_invite_link(@uri, invite.token)} + > + Copy + + +
+ + <:col :let={invite} label="Type"> + <%= invite.type %> + + <:col :let={invite} label="Valid Until"> +
+

+ <.local_time id={invite.id} at={invite.valid_until} /> +

+
+ + <:action :let={invite}> + <.button + phx-click="delete-invite" + phx-value-id={invite.id} + data={[confirm: "Please confirm to delete invite!"]} + class="hover:text-white" + > + <.icon name="hero-trash-solid" class="w-4 h-4" /> + + + + + + +
+
+
+ Tracking Pools <.table - id="invites" - rows={@invites} + id="tracking_pools" + rows={@tracker_stats} class="!max-h-[40vh] !overflow-y-auto" > - <:col :let={invite} label="Link"> -
-
<%= get_invite_link(@uri, invite.token) %>
- <.button - phx-hook="CopyToClipboard" - id="copy-to-clipboard" - class="copy-link btn btn-neutral rounded-none" - data-url={get_invite_link(@uri, invite.token)} - > - Copy - - -
+ <:col :let={stat} label="Pool"> +
{stat.title}
- <:col :let={invite} label="Type"> - <%= invite.type %> + <:col :let={stat} label="Count"> + {stat.value} - <:col :let={invite} label="Valid Until"> -
-

- <.local_time id={invite.id} at={invite.valid_until} /> -

-
- - <:action :let={invite}> - <.button - phx-click="delete-invite" - phx-value-id={invite.id} - data={[confirm: "Please confirm to delete invite!"]} - class="hover:text-white" - > - <.icon name="hero-trash-solid" class="w-4 h-4" /> - - -
diff --git a/lib/wanderer_app_web/live/characters/characters_live.ex b/lib/wanderer_app_web/live/characters/characters_live.ex index f6d8f314..b27e1ddc 100755 --- a/lib/wanderer_app_web/live/characters/characters_live.ex +++ b/lib/wanderer_app_web/live/characters/characters_live.ex @@ -5,6 +5,8 @@ defmodule WandererAppWeb.CharactersLive do alias BetterNumber, as: Number + @active_tracking_pool 1 + def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do {:ok, characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id}) @@ -56,10 +58,17 @@ defmodule WandererAppWeb.CharactersLive do @impl true def handle_event("authorize", form, socket) do track_wallet = form |> Map.get("track_wallet", false) - token = UUID.uuid4(:default) - WandererApp.Cache.put("invite_#{token}", true, ttl: :timer.minutes(30)) - {:noreply, socket |> push_navigate(to: ~p"/auth/eve?invite=#{token}&w=#{track_wallet}")} + {:ok, esi_config} = + Cachex.get( + :esi_auth_cache, + "config_#{WandererApp.Env.active_tracking_pool()}" + ) + + WandererApp.Cache.put("invite_#{esi_config.uuid}", true, ttl: :timer.minutes(30)) + + {:noreply, + socket |> push_navigate(to: ~p"/auth/eve?invite=#{esi_config.uuid}&w=#{track_wallet}")} end @impl true diff --git a/priv/repo/migrations/20250611074436_add_character_tracking_pool.exs b/priv/repo/migrations/20250611074436_add_character_tracking_pool.exs new file mode 100644 index 00000000..871ab595 --- /dev/null +++ b/priv/repo/migrations/20250611074436_add_character_tracking_pool.exs @@ -0,0 +1,21 @@ +defmodule WandererApp.Repo.Migrations.AddCharacterTrackingPool 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(:character_v1) do + add :tracking_pool, :text + end + end + + def down do + alter table(:character_v1) do + remove :tracking_pool + end + end +end diff --git a/priv/resource_snapshots/repo/character_v1/20250611074436.json b/priv/resource_snapshots/repo/character_v1/20250611074436.json new file mode 100644 index 00000000..b8252d9d --- /dev/null +++ b/priv/resource_snapshots/repo/character_v1/20250611074436.json @@ -0,0 +1,343 @@ +{ + "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": "eve_id", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "online", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "deleted", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "scopes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "character_owner_hash", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "token_type", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "expires_at", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "ship_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "ship_item_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "corporation_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "corporation_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "corporation_ticker", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "alliance_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "alliance_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "alliance_ticker", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "tracking_pool", + "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": "character_v1_user_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "user_v1" + }, + "size": null, + "source": "user_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_eve_wallet_balance", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_location", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_ship", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_solar_system_id", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_structure_id", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_station_id", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_access_token", + "type": "binary" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "encrypted_refresh_token", + "type": "binary" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "4C6974D764592ED3279CFD3FF85CE89B146C7E16628F0A4278E319A4A5E5FD8C", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "character_v1_unique_eve_id_index", + "keys": [ + { + "type": "atom", + "value": "eve_id" + } + ], + "name": "unique_eve_id", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.WandererApp.Repo", + "schema": null, + "table": "character_v1" +} \ No newline at end of file