08e9e07458
TaxBaik CI/CD / build-and-deploy (push) Successful in 47s
**Problem:**
TokenRefreshHandler (DelegatingHandler) runs on a non-circuit thread.
ILocalStorageService (JS interop) only works during component render.
Production: 401 response → token refresh → JS interop fails silently.
**Solution:**
1. ITokenStore - Scoped in-memory token store (no JS interop)
- Properties: AccessToken, RefreshToken, TokenExpiryTicks
- Method: IsAccessTokenExpired()
2. TokenStore implementation
- Replaces localStorage as primary token source
- DelegatingHandler reads/writes only to TokenStore
- Pages reload → GetAuthenticationStateAsync restores from localStorage
3. CustomAuthenticationStateProvider
- Accepts ITokenStore injection
- LoginAsync: Write to both TokenStore + localStorage
- LogoutAsync: Clear both
- GetAuthenticationStateAsync: Read from TokenStore first, fallback to localStorage
4. AdminDashboardClient BaseAddress fix
- Was: new Uri("/taxbaik/api/") - relative URI (runtime error)
- Now: Configured in Program.cs as absolute URI
- Program.cs: AddHttpClient(..., client => client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/"))
**Architecture:**
- TokenStore: Scoped in-memory (DelegatingHandler use)
- localStorage: Persistent (page reload recovery)
- Pattern: Server-side token management without JS interop
This fixes the cascading failure that would occur on any 401 in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
40 lines
1011 B
C#
40 lines
1011 B
C#
namespace TaxBaik.Web.Services;
|
|
|
|
/// <summary>
|
|
/// Scoped in-memory token store for Blazor Server.
|
|
/// SOLID: Single Responsibility - Token lifecycle management
|
|
/// Avoids JS interop from DelegatingHandler (which runs on non-circuit thread)
|
|
/// </summary>
|
|
public interface ITokenStore
|
|
{
|
|
string? AccessToken { get; set; }
|
|
string? RefreshToken { get; set; }
|
|
long? TokenExpiryTicks { get; set; }
|
|
|
|
bool IsAccessTokenExpired();
|
|
void Clear();
|
|
}
|
|
|
|
public class TokenStore : ITokenStore
|
|
{
|
|
public string? AccessToken { get; set; }
|
|
public string? RefreshToken { get; set; }
|
|
public long? TokenExpiryTicks { get; set; }
|
|
|
|
public bool IsAccessTokenExpired()
|
|
{
|
|
if (TokenExpiryTicks == null)
|
|
return true;
|
|
|
|
var expiryTime = new DateTime(TokenExpiryTicks.Value, DateTimeKind.Utc);
|
|
return expiryTime <= DateTime.UtcNow;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
AccessToken = null;
|
|
RefreshToken = null;
|
|
TokenExpiryTicks = null;
|
|
}
|
|
}
|