feat(admin): restore Blazor WebAssembly architecture for admin pages with hybrid Server routing
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m40s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m40s
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
global using System.Net.Http;
|
||||
global using System.Net.Http.Json;
|
||||
@@ -0,0 +1,13 @@
|
||||
@* WASM 기반(M3) 검증용 컴포넌트. 라우팅/렌더모드 전면 적용은 M4에서 처리한다. *@
|
||||
@rendermode InteractiveWebAssembly
|
||||
|
||||
<MudPaper Class="pa-6 ma-4" Elevation="2">
|
||||
<MudText Typo="Typo.h5" GutterBottom="true">WebAssembly 렌더 모드 점검</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-4">이 컴포넌트가 클릭에 반응하면 Interactive WebAssembly 기반이 정상 동작하는 것입니다.</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Increment">카운트: @count</MudButton>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private int count;
|
||||
private void Increment() => count++;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using MudBlazor.Services;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Web.Services;
|
||||
using TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
// MudBlazor (WASM 측 인터랙티브 컴포넌트용)
|
||||
builder.Services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.HideTransitionDuration = 400;
|
||||
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
||||
config.PopoverOptions.ThrowOnDuplicateProvider = false;
|
||||
});
|
||||
|
||||
// API Base Url 동적 구성 (호스트 기준 /taxbaik/api/)
|
||||
var apiBaseUrl = builder.HostEnvironment.BaseAddress.TrimEnd('/') + "/taxbaik/api/";
|
||||
|
||||
// HTTP Client for API (with automatic token refresh)
|
||||
builder.Services.AddScoped<ITokenStore, TokenStore>();
|
||||
builder.Services.AddScoped<TokenRefreshHandler>();
|
||||
|
||||
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
}).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
|
||||
// 각 Browser API Client 등록
|
||||
builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<ITaxProfileBrowserClient, TaxProfileBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<ITaxFilingScheduleBrowserClient, TaxFilingScheduleBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IConsultingActivityBrowserClient, ConsultingActivityBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IContractBrowserClient, ContractBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<IRevenueTrackingBrowserClient, RevenueTrackingBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
builder.Services.AddHttpClient<ICommonCodeBrowserClient, CommonCodeBrowserClient>(client => client.BaseAddress = new Uri(apiBaseUrl)).AddHttpMessageHandler<TokenRefreshHandler>();
|
||||
|
||||
// Blazor 인증 (WASM 측 클라이언트)
|
||||
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CustomAuthenticationStateProvider>());
|
||||
builder.Services.AddScoped<ILocalStorageService, LocalStorageService>();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddAuthorizationCore();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public interface ICommonCodeBrowserClient
|
||||
{
|
||||
Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default);
|
||||
Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class CommonCodeBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<CommonCodeBrowserClient> logger) : ICommonCodeBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/commoncode";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<CommonCode>> GetAllActiveAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get all active common codes");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CommonCode>> GetByGroupAsync(string group, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<CommonCode>>($"{BaseUrl}/group/{group}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get common codes for group {Group}", group);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IConsultingActivityBrowserClient
|
||||
{
|
||||
Task<List<ConsultingActivity>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<List<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<List<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken ct = default);
|
||||
Task<int> CreateAsync(int clientId, string activityType, DateTime activityDate, string description,
|
||||
int? consultantId = null, DateTime? nextFollowupDate = null, CancellationToken ct = default);
|
||||
Task UpdateAsync(int id, string? outcome = null, DateTime? nextFollowupDate = null, CancellationToken ct = default);
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class ConsultingActivityBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<ConsultingActivityBrowserClient> logger)
|
||||
: IConsultingActivityBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/consultingactivity";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<ConsultingActivity>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get consulting activities");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<ConsultingActivity>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get activities for client {ClientId}", clientId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending-followups", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<ConsultingActivity>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get pending followups");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(int clientId, string activityType, DateTime activityDate, string description,
|
||||
int? consultantId = null, DateTime? nextFollowupDate = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { clientId, activityType, activityDate, description, consultantId, nextFollowupDate };
|
||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
|
||||
return result.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create consulting activity");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(int id, string? outcome = null, DateTime? nextFollowupDate = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { outcome, nextFollowupDate };
|
||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update consulting activity {Id}", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete consulting activity {Id}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IContractBrowserClient
|
||||
{
|
||||
Task<List<Contract>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<Contract?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<List<Contract>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<List<Contract>> GetActiveContractsAsync(CancellationToken ct = default);
|
||||
Task<List<Contract>> GetExpiringContractsAsync(int daysAhead = 30, CancellationToken ct = default);
|
||||
Task<decimal> GetMonthlyRecurringRevenueAsync(CancellationToken ct = default);
|
||||
Task<int> CreateAsync(int clientId, string contractNumber, string serviceType, DateTime startDate,
|
||||
decimal? monthlyFee = null, decimal? totalAmount = null, CancellationToken ct = default);
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class ContractBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<ContractBrowserClient> logger)
|
||||
: IContractBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/contract";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<Contract>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get contracts");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Contract?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<Contract>($"{BaseUrl}/{id}", ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get contract {Id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Contract>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<Contract>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get contracts for client {ClientId}", clientId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Contract>> GetActiveContractsAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/active", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get active contracts");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Contract>> GetExpiringContractsAsync(int daysAhead = 30, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/expiring?daysAhead={daysAhead}", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<Contract>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get expiring contracts");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> GetMonthlyRecurringRevenueAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/mrr", ct);
|
||||
if (response.TryGetProperty("mrr", out var mrrValue))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<decimal>(mrrValue.GetRawText());
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get MRR");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(int clientId, string contractNumber, string serviceType, DateTime startDate,
|
||||
decimal? monthlyFee = null, decimal? totalAmount = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { clientId, contractNumber, serviceType, startDate, monthlyFee, totalAmount };
|
||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
|
||||
return result.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create contract");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete contract {Id}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IRevenueTrackingBrowserClient
|
||||
{
|
||||
Task<List<RevenueTracking>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<List<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<List<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken ct = default);
|
||||
Task<List<RevenueTracking>> GetMonthlyRevenueAsync(int year, int month, CancellationToken ct = default);
|
||||
Task<decimal> GetTotalRevenueAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default);
|
||||
Task<int> CreateAsync(int clientId, string invoiceNumber, DateTime invoiceDate, decimal amount,
|
||||
string? serviceType = null, DateTime? dueDate = null, CancellationToken ct = default);
|
||||
Task MarkPaidAsync(int id, DateTime paymentDate, CancellationToken ct = default);
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class RevenueTrackingBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<RevenueTrackingBrowserClient> logger)
|
||||
: IRevenueTrackingBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/revenuetracking";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<RevenueTracking>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get revenue tracking");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<RevenueTracking>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get revenue for client {ClientId}", clientId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/pending", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get pending payments");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<RevenueTracking>> GetMonthlyRevenueAsync(int year, int month, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/monthly?year={year}&month={month}", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<RevenueTracking>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get monthly revenue {Year}-{Month}", year, month);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalRevenueAsync(DateTime startDate, DateTime endDate, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>(
|
||||
$"{BaseUrl}/total?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}", ct);
|
||||
if (response.TryGetProperty("total", out var totalValue))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<decimal>(totalValue.GetRawText());
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get total revenue");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(int clientId, string invoiceNumber, DateTime invoiceDate, decimal amount,
|
||||
string? serviceType = null, DateTime? dueDate = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { clientId, invoiceNumber, invoiceDate, amount, serviceType, dueDate };
|
||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
|
||||
return result.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create revenue tracking");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkPaidAsync(int id, DateTime paymentDate, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { paymentDate };
|
||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/paid", request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to mark payment {Id}", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete revenue tracking {Id}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface ITaxFilingScheduleBrowserClient
|
||||
{
|
||||
Task<List<TaxFilingSchedule>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<List<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<List<TaxFilingSchedule>> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken ct = default);
|
||||
Task<int> CreateAsync(int clientId, string filingType, DateTime dueDate, int filingYear,
|
||||
int? assignedTo = null, CancellationToken ct = default);
|
||||
Task MarkCompletedAsync(int id, CancellationToken ct = default);
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class TaxFilingScheduleBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<TaxFilingScheduleBrowserClient> logger)
|
||||
: ITaxFilingScheduleBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/taxfilingschedule";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<TaxFilingSchedule>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get tax filing schedules");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<TaxFilingSchedule>($"{BaseUrl}/{id}", ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get tax filing schedule {Id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<TaxFilingSchedule>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get filing schedules for client {ClientId}", clientId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaxFilingSchedule>> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming?daysAhead={daysAhead}", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxFilingSchedule>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get upcoming filings");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(int clientId, string filingType, DateTime dueDate, int filingYear,
|
||||
int? assignedTo = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { clientId, filingType, dueDate, filingYear, assignedTo };
|
||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
|
||||
return result.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create tax filing schedule");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkCompletedAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}/complete", new { }, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to mark filing as completed {Id}", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete tax filing schedule {Id}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
namespace TaxBaik.Web.Services.AdminClients;
|
||||
|
||||
using System.Text.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface ITaxProfileBrowserClient
|
||||
{
|
||||
Task<List<TaxProfile>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<TaxProfile?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<List<TaxProfile>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<List<TaxProfile>> GetHighRiskProfilesAsync(CancellationToken ct = default);
|
||||
Task<List<TaxProfile>> GetUpcomingFilingDuesAsync(int daysAhead = 30, CancellationToken ct = default);
|
||||
Task<int> CreateAsync(int clientId, string businessType, string? businessRegistration = null,
|
||||
string? accountingMethod = null, DateTime? establishmentDate = null, CancellationToken ct = default);
|
||||
Task UpdateAsync(int id, string? businessType = null, string? accountingMethod = null,
|
||||
DateTime? nextFilingDueDate = null, string? taxRiskLevel = null, CancellationToken ct = default);
|
||||
Task DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class TaxProfileBrowserClient(HttpClient httpClient, ITokenStore tokenStore, ILogger<TaxProfileBrowserClient> logger) : ITaxProfileBrowserClient
|
||||
{
|
||||
private const string BaseUrl = "/api/taxprofile";
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
else
|
||||
httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<List<TaxProfile>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get tax profiles");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxProfile?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<TaxProfile>($"{BaseUrl}/{id}", ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get tax profile {Id}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaxProfile>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await httpClient.GetFromJsonAsync<List<TaxProfile>>($"{BaseUrl}/client/{clientId}", ct) ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get tax profiles for client {ClientId}", clientId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaxProfile>> GetHighRiskProfilesAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/high-risk", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get high-risk profiles");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TaxProfile>> GetUpcomingFilingDuesAsync(int daysAhead = 30, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.GetFromJsonAsync<JsonElement>($"{BaseUrl}/upcoming-filings?daysAhead={daysAhead}", ct);
|
||||
if (response.TryGetProperty("data", out var data))
|
||||
return System.Text.Json.JsonSerializer.Deserialize<List<TaxProfile>>(data.GetRawText()) ?? [];
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get upcoming filings");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(int clientId, string businessType, string? businessRegistration = null,
|
||||
string? accountingMethod = null, DateTime? establishmentDate = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { clientId, businessType, businessRegistration, accountingMethod, establishmentDate };
|
||||
var response = await httpClient.PostAsJsonAsync(BaseUrl, request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct);
|
||||
return result.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create tax profile");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(int id, string? businessType = null, string? accountingMethod = null,
|
||||
DateTime? nextFilingDueDate = null, string? taxRiskLevel = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { businessType, accountingMethod, nextFilingDueDate, taxRiskLevel };
|
||||
var response = await httpClient.PutAsJsonAsync($"{BaseUrl}/{id}", request, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to update tax profile {Id}", id);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await httpClient.DeleteAsync($"{BaseUrl}/{id}", ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete tax profile {Id}", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Application.Services;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Admin Dashboard API Client
|
||||
/// SOLID: Single Responsibility - Dashboard API 호출만 담당
|
||||
/// Dependency Inversion - 추상화된 인터페이스 사용
|
||||
/// </summary>
|
||||
public interface IAdminDashboardClient
|
||||
{
|
||||
Task<AdminDashboardSummary> GetSummaryAsync(CancellationToken ct = default);
|
||||
Task<IEnumerable<TaxFiling>> GetUpcomingFilingsAsync(int days = 30, CancellationToken ct = default);
|
||||
Task<IEnumerable<Inquiry>> GetRecentInquiriesAsync(int limit = 10, CancellationToken ct = default);
|
||||
Task<object> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class AdminDashboardClient : IAdminDashboardClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<AdminDashboardClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public AdminDashboardClient(HttpClient http, ILogger<AdminDashboardClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<AdminDashboardSummary> GetSummaryAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<AdminDashboardSummary>(
|
||||
"admin-dashboard/summary", cancellationToken: ct);
|
||||
return result ?? new(0, 0, 0, 0, []);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch dashboard summary");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetUpcomingFilingsAsync(int days = 30, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<ApiResponse<TaxFiling>>(
|
||||
$"admin-dashboard/upcoming-filings?days={days}", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch upcoming filings");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Inquiry>> GetRecentInquiriesAsync(int limit = 10, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<ApiResponse<Inquiry>>(
|
||||
$"admin-dashboard/recent-inquiries?limit={limit}", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch recent inquiries");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var url = "admin-dashboard/monthly-stats";
|
||||
if (!string.IsNullOrEmpty(month))
|
||||
url += $"?month={month}";
|
||||
|
||||
var result = await _http.GetFromJsonAsync<object>(url, cancellationToken: ct);
|
||||
return result ?? new();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch monthly stats");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API Response wrapper
|
||||
/// </summary>
|
||||
internal class ApiResponse<T>
|
||||
{
|
||||
public IEnumerable<T>? Data { get; set; }
|
||||
public int Total { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IAnnouncementBrowserClient
|
||||
{
|
||||
Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<Announcement?> CreateAsync(AnnouncementDto dto, CancellationToken ct = default);
|
||||
Task<Announcement?> UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class AnnouncementBrowserClient : IAnnouncementBrowserClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<AnnouncementBrowserClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public AnnouncementBrowserClient(HttpClient http, ILogger<AnnouncementBrowserClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<AnnouncementListResponse>("announcement", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch announcements");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await _http.GetFromJsonAsync<Announcement>($"announcement/{id}", cancellationToken: ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch announcement {AnnouncementId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Announcement?> CreateAsync(AnnouncementDto dto, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PostAsJsonAsync("announcement", dto, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Announcement>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create announcement");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Announcement?> UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PutAsJsonAsync($"announcement/{id}", dto, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Announcement>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update announcement {AnnouncementId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.DeleteAsync($"announcement/{id}", cancellationToken: ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete announcement {AnnouncementId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class AnnouncementListResponse
|
||||
{
|
||||
public List<Announcement> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Text.Json;
|
||||
|
||||
public interface IApiClient
|
||||
{
|
||||
Task<T?> GetAsync<T>(string endpoint);
|
||||
Task<T?> PostAsync<T>(string endpoint, object data);
|
||||
Task<T?> PutAsync<T>(string endpoint, object data);
|
||||
Task DeleteAsync(string endpoint);
|
||||
Task SetAuthToken(string? token);
|
||||
}
|
||||
|
||||
public class ApiClient : IApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private string? _authToken;
|
||||
|
||||
public ApiClient(HttpClient httpClient, NavigationManager navigationManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_navigationManager = navigationManager;
|
||||
}
|
||||
|
||||
public async Task SetAuthToken(string? token)
|
||||
{
|
||||
_authToken = token;
|
||||
if (token != null)
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new("Bearer", token);
|
||||
else
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<T?> GetAsync<T>(string endpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(BuildApiUri(endpoint));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return default;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T?> PostAsync<T>(string endpoint, object data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(BuildApiUri(endpoint), content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return default;
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T?> PutAsync<T>(string endpoint, object data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PutAsync(BuildApiUri(endpoint), content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return default;
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string endpoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _httpClient.DeleteAsync(BuildApiUri(endpoint));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private Uri BuildApiUri(string endpoint)
|
||||
{
|
||||
var relative = $"api/{endpoint.TrimStart('/')}";
|
||||
return new Uri(new Uri(_navigationManager.BaseUri), relative);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Application.DTOs;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Client API Client for Admin Blazor
|
||||
/// SOLID: Single Responsibility - Client API calls only
|
||||
/// </summary>
|
||||
public interface IClientBrowserClient
|
||||
{
|
||||
Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
|
||||
int page = 1, int pageSize = 20, string? status = null, string? search = null, CancellationToken ct = default);
|
||||
Task<Client?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<Client?> CreateAsync(CreateClientDto dto, CancellationToken ct = default);
|
||||
Task<Client?> UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class ClientBrowserClient : IClientBrowserClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<ClientBrowserClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public ClientBrowserClient(HttpClient http, ILogger<ClientBrowserClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
|
||||
int page = 1, int pageSize = 20, string? status = null, string? search = null, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var query = $"client?page={page}&pageSize={pageSize}";
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
query += $"&status={status}";
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
query += $"&search={Uri.EscapeDataString(search)}";
|
||||
|
||||
var result = await _http.GetFromJsonAsync<ClientPagedResponse>(query, cancellationToken: ct);
|
||||
return result != null ? (result.Data, result.Total) : ([], 0);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch clients");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Client?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await _http.GetFromJsonAsync<Client>($"client/{id}", cancellationToken: ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch client {ClientId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Client?> CreateAsync(CreateClientDto dto, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PostAsJsonAsync("client", dto, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Client>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create client");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Client?> UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PutAsJsonAsync($"client/{id}", dto, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Client>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update client {ClientId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.DeleteAsync($"client/{id}", cancellationToken: ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete client {ClientId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class ClientPagedResponse
|
||||
{
|
||||
public List<Client> Data { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using TaxBaik.Application.Services;
|
||||
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
private readonly IApiClient _apiClient;
|
||||
private readonly ILogger<CustomAuthenticationStateProvider> _logger;
|
||||
|
||||
public CustomAuthenticationStateProvider(
|
||||
ILocalStorageService localStorage,
|
||||
ITokenStore tokenStore,
|
||||
IApiClient apiClient,
|
||||
ILogger<CustomAuthenticationStateProvider> logger)
|
||||
{
|
||||
_localStorage = localStorage;
|
||||
_tokenStore = tokenStore;
|
||||
_apiClient = apiClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var accessToken = _tokenStore.AccessToken;
|
||||
|
||||
// TokenStore가 비어있으면 localStorage에서 복원 (페이지 리로드 후)
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
var storedToken = await _localStorage.GetItemAsStringAsync("accessToken");
|
||||
if (!string.IsNullOrEmpty(storedToken))
|
||||
{
|
||||
var refreshToken = await _localStorage.GetItemAsStringAsync("refreshToken");
|
||||
var ticksStr = await _localStorage.GetItemAsStringAsync("tokenExpiry");
|
||||
if (long.TryParse(ticksStr, out var ticks))
|
||||
{
|
||||
_tokenStore.AccessToken = storedToken;
|
||||
_tokenStore.RefreshToken = refreshToken;
|
||||
_tokenStore.TokenExpiryTicks = ticks;
|
||||
accessToken = storedToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
{
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
// 토큰이 만료되면 로그아웃
|
||||
if (_tokenStore.IsAccessTokenExpired())
|
||||
{
|
||||
_logger.LogWarning("Access token 만료됨 - 자동 로그아웃");
|
||||
await LogoutAsync();
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
// 토큰이 5분 이내로 만료되면 자동 갱신 시도 (사용자 경험 향상)
|
||||
if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken())
|
||||
{
|
||||
_logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작");
|
||||
var request = new { RefreshToken = _tokenStore.RefreshToken };
|
||||
var newTokenPair = await _apiClient.PostAsync<WasmAuthTokenPair>("auth/refresh", request);
|
||||
if (newTokenPair != null && !string.IsNullOrEmpty(newTokenPair.AccessToken))
|
||||
{
|
||||
await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn);
|
||||
_logger.LogInformation("토큰 자동 갱신 성공");
|
||||
accessToken = newTokenPair.AccessToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("토큰 자동 갱신 실패 - 로그아웃");
|
||||
await LogoutAsync();
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
}
|
||||
|
||||
var principal = ValidateTokenWithoutDb(accessToken ?? string.Empty);
|
||||
if (principal == null)
|
||||
{
|
||||
await LogoutAsync();
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
return new AuthenticationState(principal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "인증 상태 조회 중 오류 발생");
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
}
|
||||
|
||||
private ClaimsPrincipal? ValidateTokenWithoutDb(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(token);
|
||||
var identity = new ClaimsIdentity(jwtToken.Claims, "jwt");
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn)
|
||||
{
|
||||
var tokenExpiryTicks = DateTime.UtcNow.AddSeconds(expiresIn).Ticks;
|
||||
|
||||
// TokenStore에 저장 (DelegatingHandler에서 사용)
|
||||
_tokenStore.AccessToken = accessToken;
|
||||
_tokenStore.RefreshToken = refreshToken;
|
||||
_tokenStore.TokenExpiryTicks = tokenExpiryTicks;
|
||||
|
||||
// localStorage에도 저장 (페이지 리로드 후 복원)
|
||||
await _localStorage.SetItemAsStringAsync("accessToken", accessToken);
|
||||
await _localStorage.SetItemAsStringAsync("refreshToken", refreshToken);
|
||||
await _localStorage.SetItemAsStringAsync("tokenExpiry", tokenExpiryTicks.ToString());
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
private bool ShouldRefreshToken()
|
||||
{
|
||||
// 토큰이 5분 이내로 만료되면 갱신 (300초 = 5분)
|
||||
if (!_tokenStore.TokenExpiryTicks.HasValue || _tokenStore.TokenExpiryTicks.Value <= 0)
|
||||
return false;
|
||||
|
||||
const int refreshThresholdSeconds = 300;
|
||||
try
|
||||
{
|
||||
var expiryTime = new DateTime(_tokenStore.TokenExpiryTicks.Value, DateTimeKind.Utc);
|
||||
var timeUntilExpiry = expiryTime - DateTime.UtcNow;
|
||||
return timeUntilExpiry.TotalSeconds <= refreshThresholdSeconds && timeUntilExpiry.TotalSeconds > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
// TokenStore 초기화
|
||||
_tokenStore.Clear();
|
||||
|
||||
// localStorage 초기화
|
||||
await _localStorage.RemoveItemAsync("accessToken");
|
||||
await _localStorage.RemoveItemAsync("refreshToken");
|
||||
await _localStorage.RemoveItemAsync("tokenExpiry");
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
private bool IsTokenExpired(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = handler.ReadJwtToken(token);
|
||||
return jwtToken.ValidTo < DateTime.UtcNow;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class WasmAuthTokenPair
|
||||
{
|
||||
public WasmAuthTokenPair() { }
|
||||
public WasmAuthTokenPair(string accessToken, string refreshToken, int expiresIn)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
ExpiresIn = expiresIn;
|
||||
}
|
||||
public string AccessToken { get; set; } = "";
|
||||
public string RefreshToken { get; set; } = "";
|
||||
public int ExpiresIn { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
public interface IFaqBrowserClient
|
||||
{
|
||||
Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<Faq?> CreateAsync(Faq faq, CancellationToken ct = default);
|
||||
Task<Faq?> UpdateAsync(int id, Faq faq, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class FaqBrowserClient : IFaqBrowserClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<FaqBrowserClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public FaqBrowserClient(HttpClient http, ILogger<FaqBrowserClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<FaqListResponse>("faq", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch FAQs");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await _http.GetFromJsonAsync<Faq>($"faq/{id}", cancellationToken: ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch FAQ {FaqId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Faq?> CreateAsync(Faq faq, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PostAsJsonAsync("faq", faq, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Faq>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create FAQ");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Faq?> UpdateAsync(int id, Faq faq, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PutAsJsonAsync($"faq/{id}", faq, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<Faq>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update FAQ {FaqId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.DeleteAsync($"faq/{id}", cancellationToken: ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete FAQ {FaqId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class FaqListResponse
|
||||
{
|
||||
public List<Faq> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public interface ILocalStorageService
|
||||
{
|
||||
Task<string?> GetItemAsStringAsync(string key);
|
||||
Task SetItemAsStringAsync(string key, string value);
|
||||
Task RemoveItemAsync(string key);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Inquiry API Client for Admin Blazor
|
||||
/// SOLID: Single Responsibility - Inquiry API calls only
|
||||
/// Dependency Inversion - abstraction via interface
|
||||
/// </summary>
|
||||
public interface IInquiryBrowserClient
|
||||
{
|
||||
Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(int page = 1, int pageSize = 20, CancellationToken ct = default);
|
||||
Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<bool> UpdateStatusAsync(int id, string status, CancellationToken ct = default);
|
||||
Task<bool> UpdateAdminMemoAsync(int id, string adminMemo, CancellationToken ct = default);
|
||||
Task<int> ConvertToClientAsync(int id, string name, string phone, string serviceType, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class InquiryBrowserClient : IInquiryBrowserClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<InquiryBrowserClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public InquiryBrowserClient(HttpClient http, ILogger<InquiryBrowserClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<(IEnumerable<Inquiry> Items, int Total)> GetPagedAsync(
|
||||
int page = 1, int pageSize = 20, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<InquiryPagedResponse>(
|
||||
$"inquiry?page={page}&pageSize={pageSize}",
|
||||
cancellationToken: ct);
|
||||
|
||||
return result != null
|
||||
? (result.Data, result.Total)
|
||||
: ([], 0);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch inquiries");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Inquiry?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await _http.GetFromJsonAsync<Inquiry>(
|
||||
$"inquiry/{id}",
|
||||
cancellationToken: ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch inquiry {InquiryId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateStatusAsync(int id, string status, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { status };
|
||||
var response = await _http.PutAsJsonAsync(
|
||||
$"inquiry/{id}/status",
|
||||
request,
|
||||
cancellationToken: ct);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update inquiry {InquiryId} status", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAdminMemoAsync(int id, string adminMemo, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var request = new { adminMemo };
|
||||
var response = await _http.PutAsJsonAsync(
|
||||
$"inquiry/{id}/memo",
|
||||
request,
|
||||
cancellationToken: ct);
|
||||
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update inquiry {InquiryId} memo", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> ConvertToClientAsync(int id, string name, string phone, string serviceType, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PostAsJsonAsync(
|
||||
$"inquiry/{id}/convert-to-client",
|
||||
new { name, phone, serviceType },
|
||||
cancellationToken: ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return 0;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<ConvertToClientResponse>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return result?.ClientId ?? 0;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to convert inquiry {InquiryId} to client", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class InquiryPagedResponse
|
||||
{
|
||||
public List<Inquiry> Data { get; set; } = [];
|
||||
public int Total { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
|
||||
private class ConvertToClientResponse
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public class LocalStorageService : ILocalStorageService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
public LocalStorageService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<string?> GetItemAsStringAsync(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetItemAsStringAsync(string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async Task RemoveItemAsync(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using TaxBaik.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// TaxFiling API Client for Admin Blazor
|
||||
/// </summary>
|
||||
public interface ITaxFilingBrowserClient
|
||||
{
|
||||
Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default);
|
||||
Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
|
||||
Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<TaxFiling?> CreateAsync(TaxFiling filing, CancellationToken ct = default);
|
||||
Task<TaxFiling?> UpdateAsync(int id, TaxFiling filing, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class TaxFilingBrowserClient : ITaxFilingBrowserClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<TaxFilingBrowserClient> _logger;
|
||||
private readonly ITokenStore _tokenStore;
|
||||
|
||||
public TaxFilingBrowserClient(HttpClient http, ILogger<TaxFilingBrowserClient> logger, ITokenStore tokenStore)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
private void EnsureAuthHeader()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_tokenStore.AccessToken))
|
||||
_http.DefaultRequestHeaders.Authorization = new("Bearer", _tokenStore.AccessToken);
|
||||
else
|
||||
_http.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
||||
$"taxfiling/upcoming?daysAhead={daysAhead}", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch upcoming filings");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
|
||||
$"taxfiling/client/{clientId}", cancellationToken: ct);
|
||||
return result?.Data ?? [];
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch filings for client {ClientId}", clientId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
return await _http.GetFromJsonAsync<TaxFiling>(
|
||||
$"taxfiling/{id}", cancellationToken: ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch filing {FilingId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxFiling?> CreateAsync(TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PostAsJsonAsync("taxfiling", filing, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<TaxFiling>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create filing");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxFiling?> UpdateAsync(int id, TaxFiling filing, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.PutAsJsonAsync($"taxfiling/{id}", filing, cancellationToken: ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<TaxFiling>(
|
||||
content,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update filing {FilingId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureAuthHeader();
|
||||
var response = await _http.DeleteAsync($"taxfiling/{id}", cancellationToken: ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete filing {FilingId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class TaxFilingListResponse
|
||||
{
|
||||
public List<TaxFiling> Data { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청 시 자동으로 access token을 추가하고,
|
||||
/// 401 응답을 받으면 refresh token으로 새 토큰을 획득한 후 재시도합니다.
|
||||
/// SOLID: Single Responsibility - 토큰 갱신 로직만 담당
|
||||
/// </summary>
|
||||
public class TokenRefreshHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<TokenRefreshHandler> _logger;
|
||||
|
||||
public TokenRefreshHandler(IServiceProvider serviceProvider, ILogger<TokenRefreshHandler> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 최신 Scoped ITokenStore 실시간 해석 (Scope Capture 차단 및 기존 Blazor 회로 수명 공유)
|
||||
var tokenStore = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<ITokenStore>(_serviceProvider);
|
||||
|
||||
// 요청에 access token 추가
|
||||
if (!string.IsNullOrEmpty(tokenStore.AccessToken))
|
||||
{
|
||||
request.Headers.Authorization = new("Bearer", tokenStore.AccessToken);
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
// 401 응답이면 토큰 갱신 시도
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tokenStore.RefreshToken))
|
||||
{
|
||||
var newTokenPair = await RefreshTokenAsync(tokenStore.RefreshToken, request, cancellationToken);
|
||||
if (newTokenPair != null)
|
||||
{
|
||||
// TokenStore에 토큰 저장
|
||||
tokenStore.AccessToken = newTokenPair.AccessToken;
|
||||
tokenStore.RefreshToken = newTokenPair.RefreshToken;
|
||||
tokenStore.TokenExpiryTicks = DateTime.UtcNow.AddSeconds(newTokenPair.ExpiresIn).Ticks;
|
||||
|
||||
// 새 토큰으로 재요청
|
||||
request.Headers.Authorization = new("Bearer", newTokenPair.AccessToken);
|
||||
response = await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("토큰 갱신 실패 - 로그아웃");
|
||||
tokenStore.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task<WasmAuthTokenPair?> RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 원래 요청의 호스트 정보 추출
|
||||
var authority = originalRequest.RequestUri?.Authority ?? "localhost:5001";
|
||||
var scheme = originalRequest.RequestUri?.Scheme ?? "http";
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var refreshUri = new Uri($"{scheme}://{authority}/taxbaik/api/auth/refresh");
|
||||
var json = JsonSerializer.Serialize(new { refreshToken });
|
||||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await httpClient.PostAsync(refreshUri, content, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning($"Token refresh failed with status {response.StatusCode}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync(ct);
|
||||
var result = JsonSerializer.Deserialize<AuthTokenResponse>(responseContent,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return result != null
|
||||
? new WasmAuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn)
|
||||
: null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception during token refresh");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AuthTokenResponse
|
||||
{
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
public int ExpiresIn { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>TaxBaik.WasmClient</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TaxBaik.Application\TaxBaik.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
|
||||
<PackageReference Include="MudBlazor" Version="6.10.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using TaxBaik.WasmClient
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
Reference in New Issue
Block a user