fix chore
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/admin/list"
|
||||
@using Atomx.Common.Constants
|
||||
@inject ILogger<AdminList> Logger
|
||||
@* @attribute [Authorize] *@
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.LocalStorage(key 名称为 "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
//});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Atomx.Common.Utils;
|
||||
using Atomx.Common.Constants;
|
||||
using Atomx.Utils.Extension;
|
||||
|
||||
namespace Atomx.Admin.Services
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 query(access_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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
@@ -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
@@ -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 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
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
Reference in New Issue
Block a user