fix chore

This commit is contained in:
2025-12-04 03:08:29 +08:00
parent c6a95fa28d
commit 4e2bb49e86
100 changed files with 641 additions and 59800 deletions

View File

@@ -111,7 +111,7 @@ else
Console.WriteLine("请求api成功");
if (!string.IsNullOrEmpty(result.Data))
{
await localStorage.SetItemAsStringAsync(StorageKeys.JWTTokenKeyName, result.Data);
await localStorage.SetItemAsStringAsync(StorageKeys.AccessToken, result.Data);
await localStorage.SetItemAsStringAsync("refreshToken", result.Data);
var authState = (AuthStateProvider as PersistentAuthenticationStateProvider);
if (authState != null)

View File

@@ -1,4 +1,5 @@
@page "/admin/list"
@using Atomx.Common.Constants
@inject ILogger<AdminList> Logger
@* @attribute [Authorize] *@

View File

@@ -6,17 +6,39 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// ע<><EFBFBD>ش洢<D8B4><E6B4A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddBlazoredLocalStorageAsSingleton();
// <20><>Ȩ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD>
builder.Services.AddScoped<IPermissionService, ClientPermissionService>();
builder.Services.AddScoped<IconsExtension>();
builder.Services.AddScoped<ILocalizationService, LocalizationClientService>();
// Token provider<65><72>WASM<53><4D>
builder.Services.AddScoped<ITokenProvider, ClientTokenProvider>();
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
builder.Services.AddScoped<AuthHeaderHandler>();
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? builder.HostEnvironment.BaseAddress);
// ע<><D7A2>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient<6E><74><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> API <20><><EFBFBD><EFBFBD>ʹ<EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
var apiBase = builder.Configuration["WebApi:ServerUrl"] ?? builder.HostEnvironment.BaseAddress;
builder.Services.AddHttpClient("ApiClient", client =>
{
client.BaseAddress = new Uri(apiBase);
})
.AddHttpMessageHandler<AuthHeaderHandler>();
// Ϊ<><CEAA><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>δ<EFBFBD><CEB4><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD> HttpClient<6E><74>AuthHeaderHandler <20>ڲ<EFBFBD> CreateClient() ʹ<><CAB9>Ĭ<EFBFBD>Ϲ<EFBFBD><CFB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Ҳע<D2B2><D7A2>Ĭ<EFBFBD><C4AC> HttpClient <20><> BaseAddress
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));
// <20><> WASM DI <20><>ע<EFBFBD><D7A2> HttpService<63><65>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient ʵ<><CAB5>
builder.Services.AddScoped<HttpService>(sp => new HttpService(sp.GetRequiredService<HttpClient>()));
builder.Services.AddAntDesign();

View File

@@ -1,4 +1,4 @@
using Atomx.Common.Utils;
using Atomx.Common.Constants;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http.Json;
using System.Security.Claims;

View File

@@ -28,7 +28,6 @@ namespace Atomx.Admin.Client.Services
public class LocalizationClientService : ILocalizationService, IAsyncDisposable
{
private readonly HttpService _httpService;
private readonly HttpClient _httpClient;
private readonly IJSRuntime _jsRuntime;
private readonly ILogger<LocalizationClientService> _logger;
@@ -40,12 +39,10 @@ namespace Atomx.Admin.Client.Services
public LocalizationClientService(
HttpService httpService,
HttpClient httpClient,
IJSRuntime jsRuntime,
ILogger<LocalizationClientService> logger)
{
_httpService = httpService;
_httpClient = httpClient;
_jsRuntime = jsRuntime;
_logger = logger;
}

View File

@@ -1,21 +1,50 @@
using Microsoft.AspNetCore.Components;
using Atomx.Admin.Client.Utils;
using Atomx.Common.Models;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Atomx.Admin.Client.Utils
{
/// <summary>
/// 请求拦截器WASM 模式主要运行在浏览器端)
/// 功能:
/// - 在每次请求时将 access token 附带到 Authorization header
/// - 在收到 401 或响应头包含 "Token-Expired" 时,尝试使用本地保存的 refresh token 调用 /api/sign/refresh
/// - 刷新成功:更新本地存储中的 accessToken/refreshToken然后重试原请求一次
/// - 刷新失败:跳转到登录页
/// 说明:
/// - 该实现依赖于 Blazored.LocalStoragekey 名称为 "accessToken" 和 "refreshToken"
/// 若你在项目中使用不同的键名,请统一替换。
/// - 为避免并发刷新,使用一个静态 SemaphoreSlim 进行序列化刷新请求。
/// </summary>
public class AuthHeaderHandler : DelegatingHandler
{
private readonly ITokenProvider _tokenProvider;
private readonly NavigationManager _navigationManager;
private readonly ILogger<AuthHeaderHandler> _logger;
private readonly ILocalStorageService _localStorage;
private readonly IHttpClientFactory _httpClientFactory;
private static readonly SemaphoreSlim _refreshLock = new(1, 1);
// 本地存储键名(可按需修改)
private const string AccessTokenKey = "accessToken";
private const string RefreshTokenKey = "refreshToken";
public AuthHeaderHandler(
ITokenProvider tokenProvider,
NavigationManager navigationManager,
ILogger<AuthHeaderHandler> logger)
ILogger<AuthHeaderHandler> logger,
ILocalStorageService localStorage,
IHttpClientFactory httpClientFactory)
{
_tokenProvider = tokenProvider;
_navigationManager = navigationManager;
_logger = logger;
_localStorage = localStorage;
_httpClientFactory = httpClientFactory;
}
protected override async Task<HttpResponseMessage> SendAsync(
@@ -23,25 +52,50 @@ namespace Atomx.Admin.Client.Utils
{
try
{
// 获取token
// 1) 尝试从 token provider 获取并添加 Authorization header
var token = await _tokenProvider.GetTokenAsync();
if (!string.IsNullOrEmpty(token))
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
else
{
_logger.LogWarning("No authentication token available for request: {Url}", request.RequestUri);
_logger.LogDebug("No token from ITokenProvider for request {Url}", request.RequestUri);
}
var response = await base.SendAsync(request, cancellationToken);
// 处理认证失败
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
// 2) 检查 401 或 Token-Expired header
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.Headers.Contains("Token-Expired"))
{
await HandleUnauthorizedAsync();
_logger.LogInformation("Unauthorized or Token-Expired detected for {Url}", request.RequestUri);
// 仅在浏览器WASM模式下自动刷新在 Server 模式交由服务器端处理
if (OperatingSystem.IsBrowser())
{
var refreshed = await TryRefreshTokenAsync(cancellationToken);
if (refreshed)
{
// 获取新的 token 并重试请求(一次)
var newToken = await _localStorage.GetItemAsync<string>(AccessTokenKey);
if (!string.IsNullOrEmpty(newToken))
{
// 克隆原始请求HttpRequestMessage 只能发送一次)
var clonedRequest = await CloneHttpRequestMessageAsync(request, newToken);
return await base.SendAsync(clonedRequest, cancellationToken);
}
}
// 刷新失败,重定向登录
_navigationManager.NavigateTo("/account/login", true);
}
else
{
// Server 模式:记录日志,允许上层中间件决定下一步(不进行自动跳转)
_logger.LogWarning("Unauthorized in server mode for {Url}", request.RequestUri);
}
}
return response;
@@ -53,19 +107,118 @@ namespace Atomx.Admin.Client.Utils
}
}
private async Task HandleUnauthorizedAsync()
/// <summary>
/// 尝试使用本地保存的 refresh token 调用刷新接口
/// API 约定:
/// POST /api/sign/refresh
/// Body: { token: "...", refreshToken: "..." }
/// 返回: AuthResponse { Token, RefreshToken, TokenExpiry }
/// </summary>
private async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken)
{
// 在WASM模式下重定向到登录页
if (OperatingSystem.IsBrowser())
// 串行化刷新,防止多请求同时触发重复刷新
await _refreshLock.WaitAsync(cancellationToken);
try
{
_navigationManager.NavigateTo("/account/login", true);
var currentAccess = await _localStorage.GetItemAsync<string>(AccessTokenKey);
var currentRefresh = await _localStorage.GetItemAsync<string>(RefreshTokenKey);
if (string.IsNullOrEmpty(currentAccess) || string.IsNullOrEmpty(currentRefresh))
{
_logger.LogInformation("No local tokens to refresh");
return false;
}
// 调用刷新接口(使用 IHttpClientFactory 创建短期 HttpClient
var client = _httpClientFactory.CreateClient(); // 默认 client建议在 Program.cs 中配置 BaseAddress
var reqModel = new
{
token = currentAccess,
refreshToken = currentRefresh
};
var reqJson = JsonSerializer.Serialize(reqModel);
using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh")
{
Content = new StringContent(reqJson, Encoding.UTF8, "application/json")
};
try
{
var resp = await client.SendAsync(req, cancellationToken);
if (!resp.IsSuccessStatusCode)
{
_logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode);
return false;
}
var json = await resp.Content.ReadAsStringAsync(cancellationToken);
var authResp = JsonSerializer.Deserialize<AuthResponse>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken))
{
_logger.LogWarning("Invalid response from refresh endpoint");
return false;
}
// 保存新的 token本地存储
await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, cancellationToken);
await _localStorage.SetItemAsync(RefreshTokenKey, authResp.RefreshToken, cancellationToken);
_logger.LogInformation("Token refreshed successfully");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while requesting token refresh");
return false;
}
}
// 在Server模式下可以执行其他操作
else
finally
{
// Server端的处理逻辑
_logger.LogWarning("Unauthorized access detected in server mode");
_refreshLock.Release();
}
}
/// <summary>
/// 复制 HttpRequestMessage 并替换 Authorization header 为新的 token
/// </summary>
private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken)
{
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
// Copy request content (if any)
if (original.Content != null)
{
var ms = new MemoryStream();
await original.Content.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
clone.Content = new StreamContent(ms);
// copy content headers
if (original.Content.Headers != null)
{
foreach (var h in original.Content.Headers)
clone.Content.Headers.Add(h.Key, h.Value);
}
}
// copy headers
foreach (var header in original.Headers)
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
// set new auth header
clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
// copy properties
foreach (var prop in original.Options)
{
// HttpRequestOptions 不直接序列化拷贝,这里通常无需处理
}
return clone;
}
}
}

View File

@@ -1,11 +1,11 @@
using Blazored.LocalStorage;
using Atomx.Common.Configuration;
using Atomx.Common.Utils;
using Atomx.Utils.Extension;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Atomx.Common.Constants;
namespace Atomx.Admin.Client.Utils
{
@@ -48,7 +48,7 @@ namespace Atomx.Admin.Client.Utils
{
try
{
var jwtToken = await _localStorage.GetItemAsStringAsync(StorageKeys.JWTTokenKeyName);
var jwtToken = await _localStorage.GetItemAsStringAsync(StorageKeys.AccessToken);
if (string.IsNullOrEmpty(jwtToken))
return await Task.FromResult(new AuthenticationState(anonymous));
else
@@ -128,8 +128,8 @@ namespace Atomx.Admin.Client.Utils
public async Task MarkUserAsLoggedOut()
{
await _localStorage.RemoveItemAsync(StorageKeys.JWTTokenKeyName);
await _localStorage.RemoveItemAsync(StorageKeys.RefreshTokenKeyName);
await _localStorage.RemoveItemAsync(StorageKeys.AccessToken);
await _localStorage.RemoveItemAsync(StorageKeys.RefreshToken);
var authState = Task.FromResult(new AuthenticationState(anonymous));

View File

@@ -1,4 +1,5 @@
using Atomx.Common.Configuration;
using Atomx.Common.Constants;
using Microsoft.JSInterop;
namespace Atomx.Admin.Client.Utils
@@ -23,7 +24,7 @@ namespace Atomx.Admin.Client.Utils
try
{
// 从localStorage或sessionStorage获取token
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", StorageKeys.JWTTokenKeyName);
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", StorageKeys.AccessToken);
}
catch
{

View File

@@ -19,7 +19,7 @@
@using Atomx.Common.Enums
@using Atomx.Common.Models
@using Atomx.Common.Entities
@using Atomx.Common.Constant
@using Atomx.Common.Constants
@using Atomx.Utils.Extension
@using Atomx.Utils.Json
@using AntDesign

View File

@@ -1,7 +1,7 @@
using Atomx.Admin.Client.Models;
using Atomx.Admin.Client.Validators;
using Atomx.Admin.Services;
using Atomx.Common.Constant;
using Atomx.Common.Constants;
using Atomx.Common.Entities;
using Atomx.Common.Models;
using Atomx.Data;
@@ -22,7 +22,7 @@ namespace Atomx.Admin.Controllers
private readonly ILogger<AddressController> _logger;
private readonly DataContext _dbContext;
private readonly IIdCreatorService _idCreator;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
private readonly IMapper _mapper;
private readonly ICacheService _cacheService;
@@ -37,7 +37,7 @@ namespace Atomx.Admin.Controllers
/// <param name="mapper"></param>
/// <param name="jwtSettings"></param>
/// <param name="cacheService"></param>
public AddressController(ILogger<AddressController> logger, IIdCreatorService idCreator, IdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService)
public AddressController(ILogger<AddressController> logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService)
{
_logger = logger;
_idCreator = idCreator;

View File

@@ -16,7 +16,7 @@ namespace Atomx.Admin.Controllers
public class AdminController : ControllerBase
{
readonly ILogger<AdminController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -29,7 +29,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public AdminController(ILogger<AdminController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public AdminController(ILogger<AdminController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;
@@ -49,6 +49,7 @@ namespace Atomx.Admin.Controllers
public IActionResult Search(AdminSearch search, int page, int size = 20)
{
Console.WriteLine($"Search Admin: {_identityService.GetUserId()}, page: {page}, size: {size}");
var startTime = search.RangeTime[0];
if (startTime != null)
{

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class AppVersionController : ControllerBase
{
readonly ILogger<AppVersionController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -30,7 +30,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public AppVersionController(ILogger<AppVersionController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public AppVersionController(ILogger<AppVersionController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -18,7 +18,7 @@ namespace Atomx.Admin.Controllers
private readonly ILogger<AreaController> _logger;
private readonly DataContext _dbContext;
private readonly IIdCreatorService _idCreator;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
private readonly IMapper _mapper;
private readonly ICacheService _cacheService;
@@ -33,7 +33,7 @@ namespace Atomx.Admin.Controllers
/// <param name="mapper"></param>
/// <param name="jwtSettings"></param>
/// <param name="cacheService"></param>
public AreaController(ILogger<AreaController> logger, IIdCreatorService idCreator, IdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService)
public AreaController(ILogger<AreaController> logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, ICacheService cacheService)
{
_logger = logger;
_idCreator = idCreator;

View File

@@ -16,14 +16,14 @@ namespace Atomx.Admin.Controllers
public class CategoryController : ControllerBase
{
readonly ILogger<CategoryController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
readonly JwtSetting _jwtSetting;
readonly ICacheService _cacheService;
public CategoryController(ILogger<CategoryController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService)
public CategoryController(ILogger<CategoryController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService)
{
_logger = logger;
_identityService = identityService;

View File

@@ -22,7 +22,7 @@ namespace Atomx.Admin.Controllers
private readonly ILogger<CurrencyController> _logger;
private readonly DataContext _dbContext;
private readonly IIdCreatorService _idCreator;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
private readonly IMapper _mapper;
private readonly JwtSetting _jwtSettings;
private readonly ICacheService _cacheService;
@@ -38,7 +38,7 @@ namespace Atomx.Admin.Controllers
/// <param name="mapper"></param>
/// <param name="jwtSettings"></param>
/// <param name="cacheService"></param>
public CurrencyController(ILogger<CurrencyController> logger, IIdCreatorService idCreator, IdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService)
public CurrencyController(ILogger<CurrencyController> logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService)
{
_logger = logger;
_idCreator = idCreator;

View File

@@ -21,7 +21,7 @@ namespace Atomx.Admin.Controllers
private readonly ILogger<LanguageController> _logger;
private readonly DataContext _dbContext;
private readonly IIdCreatorService _idCreator;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
private readonly IMapper _mapper;
private readonly JwtSetting _jwtSettings;
private readonly ICacheService _cacheService;
@@ -38,7 +38,7 @@ namespace Atomx.Admin.Controllers
/// <param name="mapper"></param>
/// <param name="jwtSettings"></param>
/// <param name="cacheService"></param>
public LanguageController(ILogger<LanguageController> logger, IIdCreatorService idCreator, IdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService, LocalizationFile localizationFile)
public LanguageController(ILogger<LanguageController> logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService, LocalizationFile localizationFile)
{
_logger = logger;
_idCreator = idCreator;

View File

@@ -19,7 +19,7 @@ namespace Atomx.Admin.Controllers
private readonly ILogger<LocaleResourceController> _logger;
private readonly DataContext _dbContext;
private readonly IIdCreatorService _idCreator;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
private readonly IMapper _mapper;
private readonly JwtSetting _jwtSettings;
private readonly ICacheService _cacheService;
@@ -36,7 +36,7 @@ namespace Atomx.Admin.Controllers
/// <param name="mapper"></param>
/// <param name="jwtSettings"></param>
/// <param name="cacheService"></param>
public LocaleResourceController(ILogger<LocaleResourceController> logger, IIdCreatorService idCreator, IdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService, LocalizationFileService localizationFileService)
public LocaleResourceController(ILogger<LocaleResourceController> logger, IIdCreatorService idCreator, IIdentityService identityService, DataContext dbContext, IMapper mapper, JwtSetting jwtSettings, ICacheService cacheService, LocalizationFileService localizationFileService)
{
_logger = logger;
_idCreator = idCreator;

View File

@@ -18,14 +18,14 @@ namespace Atomx.Admin.Controllers
public class MenuController : ControllerBase
{
readonly ILogger<MenuController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
readonly JwtSetting _jwtSetting;
readonly ICacheService _cacheService;
public MenuController(ILogger<MenuController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService)
public MenuController(ILogger<MenuController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService)
{
_logger = logger;
_identityService = identityService;

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class MessageTemplateController : ControllerBase
{
readonly ILogger<MessageTemplateController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -31,7 +31,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="messageTemplateService"></param>
/// <param name="mapper"></param>
public MessageTemplateController(ILogger<MessageTemplateController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
public MessageTemplateController(ILogger<MessageTemplateController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
{
_logger = logger;
_identityService = identityService;

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class ProductAttributeController : ControllerBase
{
readonly ILogger<ProductAttributeController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -30,7 +30,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public ProductAttributeController(ILogger<ProductAttributeController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public ProductAttributeController(ILogger<ProductAttributeController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -16,7 +16,7 @@ namespace Atomx.Admin.Controllers
public class ProductAttributeOptionController : ControllerBase
{
readonly ILogger<ProductAttributeOptionController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -29,7 +29,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public ProductAttributeOptionController(ILogger<ProductAttributeOptionController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public ProductAttributeOptionController(ILogger<ProductAttributeOptionController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -20,7 +20,7 @@ namespace Atomx.Admin.Controllers
public class ProductController : ControllerBase
{
readonly ILogger<ProductController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -33,7 +33,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public ProductController(ILogger<ProductController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public ProductController(ILogger<ProductController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -3,7 +3,7 @@ using AntDesign;
using Atomx.Admin.Client.Models;
using Atomx.Admin.Client.Validators;
using Atomx.Admin.Services;
using Atomx.Common.Constant;
using Atomx.Common.Constants;
using Atomx.Common.Entities;
using Atomx.Common.Models;
using Atomx.Data;
@@ -22,7 +22,7 @@ namespace Atomx.Admin.Controllers
public class RoleController : ControllerBase
{
readonly ILogger<RoleController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -36,7 +36,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="mapper"></param>
/// <param name="userService"></param>
public RoleController(ILogger<RoleController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
public RoleController(ILogger<RoleController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
{
_logger = logger;
_identityService = identityService;

View File

@@ -3,8 +3,8 @@ using Atomx.Admin.Client.Models;
using Atomx.Admin.Client.Validators;
using Atomx.Admin.Services;
using Atomx.Admin.Utils;
using Atomx.Common.Constants;
using Atomx.Common.Models;
using Atomx.Common.Utils;
using Atomx.Data;
using Atomx.Data.CacheServices;
using Atomx.Data.Services;
@@ -28,7 +28,7 @@ namespace Atomx.Admin.Controllers
public class SignController : ControllerBase
{
readonly ILogger<SignController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -36,7 +36,7 @@ namespace Atomx.Admin.Controllers
readonly ICacheService _cacheService;
readonly AuthenticationStateProvider _authenticationStateProvider;
public SignController(ILogger<SignController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService, AuthenticationStateProvider authenticationStateProvider)
public SignController(ILogger<SignController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService, AuthenticationStateProvider authenticationStateProvider)
{
_logger = logger;
_identityService = identityService;
@@ -126,8 +126,9 @@ namespace Atomx.Admin.Controllers
user.LastLogin = DateTime.UtcNow;
user.LastIp = _identityService.GetClientIp();
user.LoginCount++;
_dbContext.Admins.Update(user);
_dbContext.SaveChanges();
//((PersistingRevalidatingAuthenticationStateProvider) _authenticationStateProvider).
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class SiteAppController : ControllerBase
{
readonly ILogger<SiteAppController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -31,7 +31,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="messageTemplateService"></param>
/// <param name="mapper"></param>
public SiteAppController(ILogger<SiteAppController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
public SiteAppController(ILogger<SiteAppController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext, ICacheService cacheService)
{
_logger = logger;
_identityService = identityService;

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class SpecificationAttributeController : ControllerBase
{
readonly ILogger<SpecificationAttributeController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -30,7 +30,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public SpecificationAttributeController(ILogger<SpecificationAttributeController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public SpecificationAttributeController(ILogger<SpecificationAttributeController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -17,7 +17,7 @@ namespace Atomx.Admin.Controllers
public class SpecificationAttributeOptionController : ControllerBase
{
readonly ILogger<SpecificationAttributeOptionController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -30,7 +30,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public SpecificationAttributeOptionController(ILogger<SpecificationAttributeOptionController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public SpecificationAttributeOptionController(ILogger<SpecificationAttributeOptionController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -16,7 +16,7 @@ namespace Atomx.Admin.Controllers
public class UploadFileController : ControllerBase
{
readonly ILogger<UploadFileController> _logger;
readonly IdentityService _identityService;
readonly IIdentityService _identityService;
readonly IIdCreatorService _idCreator;
readonly IMapper _mapper;
readonly DataContext _dbContext;
@@ -29,7 +29,7 @@ namespace Atomx.Admin.Controllers
/// <param name="idCreator"></param>
/// <param name="adminService"></param>
/// <param name="mapper"></param>
public UploadFileController(ILogger<UploadFileController> logger, IdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
public UploadFileController(ILogger<UploadFileController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dataContext)
{
_logger = logger;
_identityService = identityService;

View File

@@ -1,7 +1,8 @@
using Atomx.Admin.Utils;
using Atomx.Common.Constant;
using Atomx.Common.Constants;
using Atomx.Common.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Text.Json;
@@ -26,112 +27,102 @@ namespace Atomx.Admin.Extensions
}
services.AddSingleton(jwtSetting);
//认证配置
services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = !environment.IsDevelopment();
options.SaveToken = true;
// 从配置读取 Cookie 设置(可在 appsettings.json 的 Authentication:Cookie 节点配置)
var cookieConf = Configuration.GetSection("Authentication:Cookie");
var cookieName = cookieConf.GetValue<string>("Name") ?? ".Atomx.Auth";
var cookiePath = cookieConf.GetValue<string>("Path") ?? "/";
var cookieDomain = cookieConf.GetValue<string>("Domain");
var sameSiteStr = cookieConf.GetValue<string>("SameSite");
var securePolicyStr = cookieConf.GetValue<string>("SecurePolicy");
var expireMinutes = cookieConf.GetValue<int?>("ExpireMinutes") ?? 60;
options.ClaimsIssuer = jwtSetting.Issuer;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = jwtSetting.Audience,//Audience
ValidIssuer = jwtSetting.Issuer,
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)), //过期时钟偏差
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
};
options.Events = new JwtBearerEvents
{
//OnTokenValidated = async context =>
//{
// var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
// var userId = context.Principal?.FindFirst("sub")?.Value;
// 解析 SameSite默认开发环境 Strict生产环境 None 用于跨站点场景比如前后端分离)
SameSiteMode sameSiteMode;
if (string.IsNullOrWhiteSpace(sameSiteStr) || !Enum.TryParse<SameSiteMode>(sameSiteStr, true, out sameSiteMode))
{
sameSiteMode = environment.IsDevelopment() ? SameSiteMode.Strict : SameSiteMode.None;
}
// if (userId != null)
// {
// var user = await userService.GetUserByIdAsync(userId);
// if (user == null || !user.IsActive)
// {
// context.Fail("用户不存在或已被禁用");
// }
// }
//},
//OnMessageReceived = context =>
//{
// // SignalR JWT支持
// var accessToken = context.Request.Query["access_token"];
// var path = context.HttpContext.Request.Path;
// 解析 SecurePolicy默认开发 SameAsRequest生产 Always
CookieSecurePolicy securePolicy;
if (string.IsNullOrWhiteSpace(securePolicyStr) || !Enum.TryParse<CookieSecurePolicy>(securePolicyStr, true, out securePolicy))
{
securePolicy = environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always;
}
// if (!string.IsNullOrEmpty(accessToken) &&
// (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/notification")))
// {
// context.Token = accessToken;
// }
// return Task.CompletedTask;
//},
//认证配置:注册 Cookie用于 SignIn/SignOut和 JwtBearer用于 API 授权)
services.AddAuthentication(options =>
{
// 默认用于 API 的认证/挑战方案使用 JwtBearer
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// Cookie 配置,确保 SignInAsync 能找到处理器
options.Cookie.Name = cookieName;
options.Cookie.Path = cookiePath;
if (!string.IsNullOrWhiteSpace(cookieDomain))
{
options.Cookie.Domain = cookieDomain;
}
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = sameSiteMode;
options.Cookie.SecurePolicy = securePolicy;
OnChallenge = context =>
{
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.HandleResponse();
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
return Task.CompletedTask;
options.ExpireTimeSpan = TimeSpan.FromMinutes(expireMinutes);
options.SlidingExpiration = true;
//context.HandleResponse();
//context.Response.StatusCode = StatusCodes.Status401Unauthorized;
//context.Response.ContentType = "application/json";
options.LoginPath = "/account/login";
options.LogoutPath = "/api/sign/out";
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = !environment.IsDevelopment();
options.SaveToken = true;
//var result = JsonSerializer.Serialize(new
//{
// StatusCode = 401,
// Message = "未授权访问",
// Error = context.Error,
// ErrorDescription = context.ErrorDescription
//});
options.ClaimsIssuer = jwtSetting.Issuer;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = jwtSetting.Audience,//Audience
ValidIssuer = jwtSetting.Issuer,
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)), //过期时钟偏差
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.HandleResponse();
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
return Task.CompletedTask;
},
//return context.Response.WriteAsync(result);
},
OnAuthenticationFailed = context =>
{
//Token expired
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
OnAuthenticationFailed = context =>
{
//Token expired
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Append("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
// 基于角色的策略
//options.AddPolicy("AdminOnly", policy =>
// policy.RequireRole("Administrator"));
//options.AddPolicy("ManagerOrAdmin", policy =>
// policy.RequireRole("Administrator", "Manager"));
// 基于权限的策略
var allPermissions = Permissions.GetAllPermissions();
foreach (var permission in allPermissions)
{
options.AddPolicy(permission, policy => { policy.Requirements.Add(new PermissionRequirement(permission)); });
}
// 组合策略
//options.AddPolicy("CanManageUsers", policy =>
//{
// policy.RequireRole("Administrator", "UserManager");
// policy.Requirements.Add(new PermissionRequirement(PermissionConstants.Users.Edit));
//});
});
}
}

View File

@@ -1,5 +1,5 @@
using Atomx.Common.Configuration;
using Atomx.Common.Constant;
using Atomx.Common.Constants;
using Atomx.Common.Entities;
using Atomx.Common.Enums;
using Atomx.Data;

View File

@@ -20,6 +20,8 @@ using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
using Serilog;
using System;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
@@ -63,7 +65,7 @@ builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAu
builder.Services.AddScoped<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
builder.Services.AddScoped<IIdCreatorService, IdCreatorService>();
builder.Services.AddScoped<IdentityService, IdentityService>();
builder.Services.AddScoped<IIdentityService, IdentityService>();
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
builder.Services.AddScoped<LocalizationFile, LocalizationFile>();
builder.Services.AddScoped<ITokenProvider, ServerTokenProvider>();
@@ -92,21 +94,17 @@ builder.Services.AddStackExchangeRedisCache(options =>
// ConnectionMultiplexer.Connect(redisConnection));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧѹ<D3A6><D1B9>
// Ϊ<><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> BrowserRefresh ע<><D7A2><EFBFBD>ű<EFBFBD><C5B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>ѹ<EFBFBD><D1B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Content-Encoding: br <20><><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>ʧ<EFBFBD>ܣ<EFBFBD>
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
// <20><EFBFBD><EFBFBD><EFBFBD>Antiforgery
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = ".Atomx.Antiforgery";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
// <20>ų<EFBFBD> text/html<6D><6C>BrowserRefresh <20><>Ҫ<EFBFBD><D2AA>δѹ<CEB4><D1B9><EFBFBD><EFBFBD> HTML <20><>ע<EFBFBD><D7A2><EFBFBD>ű<EFBFBD>
options.MimeTypes = ResponseCompressionDefaults.MimeTypes
.Where(m => !string.Equals(m, "text/html", StringComparison.OrdinalIgnoreCase))
.ToArray();
});
@@ -140,7 +138,7 @@ else
}
// <20><>ȫͷ
app.UseSecurityHeaders();
//app.UseSecurityHeaders();
// <20><>Ӧѹ<D3A6><D1B9>
app.UseResponseCompression();
@@ -163,7 +161,7 @@ app.UseStaticFiles(new StaticFileOptions
});
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
app.UseRateLimiter();
//app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -1,4 +1,4 @@
using Atomx.Common.Utils;
using Atomx.Common.Constants;
using Atomx.Utils.Extension;
namespace Atomx.Admin.Services

View File

@@ -11,14 +11,13 @@ namespace Atomx.Admin.Services
{
private readonly DataContext _dbContext;
private readonly ICacheService _cacheService;
private readonly IdentityService _identityService;
private readonly IIdentityService _identityService;
public PermissionService(DataContext dataContext, ICacheService cacheService, IdentityService identityService )
public PermissionService(DataContext dataContext, ICacheService cacheService, IIdentityService identityService )
{
_dbContext = dataContext;
_cacheService = cacheService;
_identityService = identityService;
}
//private int GetCurrentUserId()

View File

@@ -69,7 +69,7 @@ namespace Atomx.Admin.Services
Token = HashRefreshToken(refreshToken),
UserId = user.Id,
IssuedTime = DateTime.UtcNow,
ExpiresTime = DateTime.UtcNow.AddDays(_jwtSetting.RefreshTokenExpirationMinutes),
ExpiresTime = DateTime.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
Ip = ipAddress,
UserAgent = userAgent
};

View File

@@ -1,4 +1,4 @@
using Atomx.Common.Utils;
using Atomx.Common.Constants;
using Atomx.Utils.Extension;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;

View File

@@ -1,8 +1,20 @@
using Atomx.Admin.Client.Utils;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System.IdentityModel.Tokens.Jwt;
namespace Atomx.Admin.Utils
{
/// <summary>
/// Server 模式下的 Token 提供器
/// 目的:
/// - 在 Blazor Server 环境中为后端 HttpClient / 服务提供当前请求的 access token
/// - 支持从 Authorization header、SignalR queryaccess_token、cookie 或 HttpContext 的身份令牌中读取
/// - 对 JWT 做基本的过期检查(如果是 JWT 格式),以便快速判断 token 是否可用
/// 说明:
/// - 这个类只负责从当前 HttpContext 中“读取”token不做刷新之类的动作刷新留给专门的 TokenService / 客户端逻辑)。
/// - 如果没有 HttpContext例如后台任务则返回 null。
/// </summary>
public class ServerTokenProvider : ITokenProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -12,16 +24,103 @@ namespace Atomx.Admin.Utils
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 尝试从当前请求中读取 access token按优先级
/// 1. Authorization header ("Bearer ...")
/// 2. Query string "access_token"SignalR 客户端会把 token 放在这里)
/// 3. HttpContext 的认证 token (HttpContext.GetTokenAsync("access_token")) - 适配 cookie/token 保存的场景
/// 4. Cookies["access_token"]
/// 5. HttpContext.Items["access_token"](如果中间件/自定义逻辑放在这里)
/// </summary>
public async Task<string?> GetTokenAsync()
{
// 在Server端从HttpContext获取token
return await _httpContextAccessor.HttpContext?.GetTokenAsync("access_token");
var ctx = _httpContextAccessor.HttpContext;
if (ctx == null)
return null;
// 1) Authorization header
if (ctx.Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
{
var authHeader = authHeaderValues.ToString();
if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
if (!string.IsNullOrEmpty(token))
return token;
}
}
// 2) SignalR / websocket: query string access_token
if (ctx.Request.Query.TryGetValue("access_token", out var queryToken))
{
var token = queryToken.ToString();
if (!string.IsNullOrEmpty(token))
return token;
}
// 3) 从认证系统中读取(例如 UseAuthentication + SaveToken = true 的场景)
try
{
var saved = await ctx.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(saved))
return saved;
}
catch
{
// 安全地忽略错误GetTokenAsync 在某些场景下可能为 null 或抛异常)
}
// 4) Cookies如果你的系统将 token 写入 cookie通常不建议但为兼容性保留
if (ctx.Request.Cookies.TryGetValue("access_token", out var cookieToken) && !string.IsNullOrEmpty(cookieToken))
{
return cookieToken;
}
// 5) Items / 特殊存储点(某些中间件可能会放在这里)
if (ctx.Items.TryGetValue("access_token", out var itemToken) && itemToken is string sToken && !string.IsNullOrEmpty(sToken))
{
return sToken;
}
return null;
}
/// <summary>
/// 快速判断 token 是否存在且(如果是 JWT未过期
/// 注意:此判断为快速检查(不替代服务器端的完整 Token 验证)
/// </summary>
public async Task<bool> IsTokenValidAsync()
{
var token = await GetTokenAsync();
return !string.IsNullOrEmpty(token);
if (string.IsNullOrEmpty(token))
return false;
// 如果是 JWT可以解析 exp 做快速过期检查
try
{
var handler = new JwtSecurityTokenHandler();
if (handler.CanReadToken(token))
{
var jwt = handler.ReadJwtToken(token);
// exp claim 是 unix 时间seconds
var expClaim = jwt.Claims.FirstOrDefault(c => c.Type == "exp")?.Value;
if (long.TryParse(expClaim, out var expSec))
{
var exp = DateTimeOffset.FromUnixTimeSeconds(expSec).UtcDateTime;
return exp > DateTime.UtcNow;
}
// 如果没有 exp claim保守认为有效因为无法判断
return true;
}
}
catch
{
// 解析错误 -> 不影响业务,判为不可用(若不是 JWT 则无法判断)
}
// 如果不是 JWT简单返回 true存在 token 即可)
return true;
}
}
}

View File

@@ -20,6 +20,14 @@
"AccessTokenExpirationMinutes": "60",
"RefreshTokenExpirationMinutes": "60",
"MaxRefreshTokensPerUser": "3"
},
"Cookie": {
"Name": ".Atomx.Auth",
"Path": "/",
"Domain": "admin.example.com",
"SameSite": "None",
"SecurePolicy": "Always",
"ExpireMinutes": 120
}
},

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,597 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,594 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long