diff --git a/lib/wanderer_app/esi/api_client.ex b/lib/wanderer_app/esi/api_client.ex index 38890cb3..32ce7d64 100644 --- a/lib/wanderer_app/esi/api_client.ex +++ b/lib/wanderer_app/esi/api_client.ex @@ -897,33 +897,42 @@ defmodule WandererApp.Esi.ApiClient do end defp invalidate_character_tokens(character, character_id, expires_at, scopes) do - # Re-load from DB to avoid race with concurrent re-auth - case WandererApp.Api.Character.by_id(character_id) do - {:ok, current_character} -> - # Only invalidate if tokens haven't been refreshed since we started - if current_character.access_token == character.access_token do - attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes} + # Skip invalidation if the character was recently re-authorized via SSO. + # This protects fresh tokens from being wiped by transient invalid_grant + # errors that can occur shortly after re-auth. + if WandererApp.Cache.lookup!("character:#{character_id}:reauth_grace", false) do + Logger.info( + "[ApiClient] Skipping token invalidation for #{character_id} - within re-auth grace period" + ) + else + # Re-load from DB to avoid race with concurrent re-auth + case WandererApp.Api.Character.by_id(character_id) do + {:ok, current_character} -> + # Only invalidate if tokens haven't been refreshed since we started + if current_character.access_token == character.access_token do + attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes} - with {:ok, _} <- WandererApp.Api.Character.update(current_character, attrs) do - WandererApp.Character.update_character(character_id, attrs) + with {:ok, _} <- WandererApp.Api.Character.update(current_character, attrs) do + WandererApp.Character.update_character(character_id, attrs) + else + error -> + Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}") + end + + Phoenix.PubSub.broadcast( + WandererApp.PubSub, + "character:#{character_id}", + :character_token_invalid + ) else - error -> - Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}") + Logger.info( + "[ApiClient] Skipping token invalidation for #{character_id} - tokens were refreshed concurrently" + ) end - Phoenix.PubSub.broadcast( - WandererApp.PubSub, - "character:#{character_id}", - :character_token_invalid - ) - else - Logger.info( - "[ApiClient] Skipping token invalidation for #{character_id} - tokens were refreshed concurrently" - ) - end - - {:error, _} -> - Logger.error("Failed to load character #{character_id} for token invalidation") + {:error, _} -> + Logger.error("Failed to load character #{character_id} for token invalidation") + end end :ok diff --git a/lib/wanderer_app_web/controllers/auth_controller.ex b/lib/wanderer_app_web/controllers/auth_controller.ex index 1d01a914..6150051d 100644 --- a/lib/wanderer_app_web/controllers/auth_controller.ex +++ b/lib/wanderer_app_web/controllers/auth_controller.ex @@ -46,6 +46,14 @@ defmodule WandererAppWeb.AuthController do # premature token invalidation after a successful re-auth WandererApp.Cache.delete("character:#{character.id}:invalid_grant_count") + # Set a grace period to protect fresh tokens from being wiped by + # in-flight or immediately-subsequent invalid_grant errors + WandererApp.Cache.put( + "character:#{character.id}:reauth_grace", + true, + ttl: :timer.minutes(5) + ) + # Update corporation/alliance data from ESI to ensure access control is current update_character_affiliation(character) diff --git a/lib/wanderer_app_web/live/characters/characters_live.ex b/lib/wanderer_app_web/live/characters/characters_live.ex index d1b99d78..69ab552f 100755 --- a/lib/wanderer_app_web/live/characters/characters_live.ex +++ b/lib/wanderer_app_web/live/characters/characters_live.ex @@ -22,6 +22,11 @@ defmodule WandererAppWeb.CharactersLive do "character:#{character_id}:corporation" ) + Phoenix.PubSub.subscribe( + WandererApp.PubSub, + "character:#{character_id}" + ) + :ok = WandererApp.Character.TrackerManager.start_tracking(character_id) end) @@ -148,6 +153,18 @@ defmodule WandererAppWeb.CharactersLive do {:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))} end + @impl true + def handle_info( + event, + socket + ) + when event in [:character_token_invalid, :token_updated] do + {:ok, characters} = + WandererApp.Api.Character.active_by_user(%{user_id: socket.assigns.user_id}) + + {:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))} + end + @impl true def handle_info( _event,