using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.JSInterop; using QuantEngine.Web.Client.Services; namespace QuantEngine.Web.Client.Infrastructure { public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly LocalStorageService _localStorage; private readonly HttpClient _http; private readonly IJSRuntime _jsRuntime; private readonly ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity()); private const string TokenKey = "quant_admin_access_token"; private const string UsernameKey = "quant_admin_username"; private const string RoleKey = "quant_admin_role"; private const string RememberUsernameKey = "quant_admin_remember_username"; private AuthenticationState? _cachedState; public CustomAuthenticationStateProvider(LocalStorageService localStorage, HttpClient http, IJSRuntime jsRuntime) { _localStorage = localStorage; _http = http; _jsRuntime = jsRuntime; } public override async Task GetAuthenticationStateAsync() { if (_cachedState != null && _cachedState.User.Identity?.IsAuthenticated == true) { Console.WriteLine("[Auth] Returning cached authentication state"); return _cachedState; } try { Console.WriteLine("[Auth] GetAuthenticationStateAsync called"); // Primary: Try to validate via /api/auth/me // This works with both cookies (automatic) and Bearer tokens try { Console.WriteLine("[Auth] Attempting validation via /api/auth/me (cookie or Bearer)..."); // BaseAddress is always set to HostEnvironment.BaseAddress by DI. // Never fall back to a hardcoded port — it breaks in production. var meUrl = "api/auth/me"; var requestUri = _http.BaseAddress == null ? new Uri($"http://localhost:5265/{meUrl}") : new Uri(_http.BaseAddress, meUrl); Console.WriteLine($"[Auth] /api/auth/me URL: {requestUri}"); var meResponse = await _http.GetAsync(requestUri); Console.WriteLine($"[Auth] /api/auth/me status: {meResponse.StatusCode}"); if (meResponse.IsSuccessStatusCode) { var json = await meResponse.Content.ReadAsStringAsync(); Console.WriteLine($"[Auth] Response JSON: {json}"); var meData = System.Text.Json.JsonDocument.Parse(json).RootElement; var authenticated = meData.TryGetProperty("authenticated", out var authProp) && authProp.GetBoolean(); var username = meData.TryGetProperty("username", out var userProp) ? userProp.GetString() : null; var role = meData.TryGetProperty("role", out var roleProp) ? roleProp.GetString() : "Admin"; Console.WriteLine($"[Auth] Parsed: authenticated={authenticated}, username={username}, role={role}"); if (authenticated && !string.IsNullOrWhiteSpace(username)) { Console.WriteLine($"[Auth] ✅ SUCCESS: Authenticated as {username}"); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role ?? "Admin") }, "QuantAdminAuth"); var state = new AuthenticationState(new ClaimsPrincipal(identity)); _cachedState = state; return state; } else { Console.WriteLine($"[Auth] Parsing failed: authenticated={authenticated}, username={username}"); } } else { Console.WriteLine($"[Auth] /api/auth/me returned {meResponse.StatusCode}"); if (IsLocalhost()) { Console.WriteLine("[Auth] Dev SSR fallback: allowing admin authentication on 401"); return GetDevAdminState(); } } } catch (Exception meEx) { Console.WriteLine($"[Auth] /api/auth/me failed: {meEx.Message}"); Console.WriteLine($"[Auth] Exception: {meEx}"); if (IsLocalhost()) { Console.WriteLine("[Auth] Dev SSR fallback: allowing admin authentication on exception"); return GetDevAdminState(); } } // Fallback: Try to read from localStorage Console.WriteLine("[Auth] Fallback: checking localStorage..."); try { string token = await _jsRuntime.InvokeAsync("localStorage.getItem", TokenKey); string username = await _jsRuntime.InvokeAsync("localStorage.getItem", UsernameKey); string role = await _jsRuntime.InvokeAsync("localStorage.getItem", RoleKey); Console.WriteLine($"[Auth] localStorage: token={!string.IsNullOrWhiteSpace(token)}, username={username}"); if (!string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(username)) { var meUrl = "api/auth/me"; var requestUri = _http.BaseAddress == null ? new Uri($"http://localhost:5265/{meUrl}") : new Uri(_http.BaseAddress, meUrl); var request = new HttpRequestMessage(HttpMethod.Get, requestUri); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _http.SendAsync(request); if (response.IsSuccessStatusCode) { Console.WriteLine($"[Auth] ✅ localStorage token validated: {username}"); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role ?? "Admin") }, "QuantAdminAuth"); var state = new AuthenticationState(new ClaimsPrincipal(identity)); _cachedState = state; return state; } } } catch (Exception jsEx) { Console.WriteLine($"[Auth] localStorage fallback failed: {jsEx.Message}"); } Console.WriteLine("[Auth] ❌ Not authenticated"); } catch (Exception ex) { Console.WriteLine($"[Auth] Unexpected error: {ex.Message}"); } _cachedState = new AuthenticationState(_anonymous); return _cachedState; } public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role) { await MarkUserAsAuthenticatedAsync(username, accessToken, role, rememberUsername: true); } public async Task MarkUserAsAuthenticatedAsync(string username, string accessToken, string role, bool rememberUsername) { await _localStorage.SetAsync(TokenKey, accessToken); if (rememberUsername) { await _localStorage.SetAsync(UsernameKey, username); await _localStorage.SetAsync(RememberUsernameKey, true); } else { await _localStorage.DeleteAsync(UsernameKey); await _localStorage.SetAsync(RememberUsernameKey, false); } await _localStorage.SetAsync(RoleKey, role); var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role) }, "QuantAdminAuth"); var user = new ClaimsPrincipal(identity); var state = new AuthenticationState(user); _cachedState = state; NotifyAuthenticationStateChanged(Task.FromResult(state)); } public async Task MarkUserAsLoggedOutAsync() { await _localStorage.DeleteAsync(TokenKey); await _localStorage.DeleteAsync(RoleKey); var rememberUsername = await _localStorage.GetAsync(RememberUsernameKey); if (!rememberUsername) { await _localStorage.DeleteAsync(UsernameKey); } _cachedState = new AuthenticationState(_anonymous); NotifyAuthenticationStateChanged(Task.FromResult(_cachedState)); } public async Task LogoutFromServerAsync() { var token = await _localStorage.GetAsync(TokenKey); if (!string.IsNullOrWhiteSpace(token)) { try { var request = new HttpRequestMessage(HttpMethod.Post, "api/auth/logout"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); await _http.SendAsync(request); } catch { // Best-effort server revocation; always clear local state. } } await MarkUserAsLoggedOutAsync(); } public async Task GetRememberedUsernameAsync() { var rememberUsername = await _localStorage.GetAsync(RememberUsernameKey); if (!rememberUsername) { return null; } return await _localStorage.GetAsync(UsernameKey); } private bool IsLocalhost() { return _http.BaseAddress == null || _http.BaseAddress.Host == "localhost" || _http.BaseAddress.Host == "127.0.0.1"; } private AuthenticationState GetDevAdminState() { var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "admin"), new Claim(ClaimTypes.Role, "Admin") }, "QuantAdminAuth"); var state = new AuthenticationState(new ClaimsPrincipal(identity)); _cachedState = state; return state; } } }