fix localization
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Blazilla" Version="2.0.1" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
jsResult = await JS.InvokeAsync<string>("CookieReader.Read", "atomx.culture");
|
||||
jsResult = await JS.InvokeAsync<string>("cookies.Read", "atomx.culture");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,9 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Net.Http;
|
||||
using FluentValidation;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
@@ -67,6 +70,8 @@ builder.Services.AddScoped<HttpService>(sp =>
|
||||
return new HttpService(httpClient, httpContextAccessor);
|
||||
});
|
||||
|
||||
builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly);
|
||||
|
||||
builder.Services.AddAntDesign();
|
||||
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace Atomx.Admin.Client.Services
|
||||
{
|
||||
if (_jsRuntime != null && OperatingSystem.IsBrowser())
|
||||
{
|
||||
var cookieVal = await _jsRuntime.InvokeAsync<string>("CookieReader.Read", CookieName);
|
||||
var cookieVal = await _jsRuntime.InvokeAsync<string>("cookies.Read", CookieName);
|
||||
_logger?.LogDebug("Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal);
|
||||
if (!string.IsNullOrEmpty(cookieVal))
|
||||
{
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
using Atomx.Admin.Client.Models;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Atomx.Admin.Client.Validators
|
||||
{
|
||||
public class LoginModelValidator : AbstractValidator<LoginModel>
|
||||
{
|
||||
public LoginModelValidator()
|
||||
public LoginModelValidator(IStringLocalizer<LoginModelValidator> localizer)
|
||||
{
|
||||
RuleFor(p => p.Account).NotEmpty().WithMessage("登录账号不能为空");
|
||||
RuleFor(p => p.Account).Length(2, 100).When(p => !string.IsNullOrEmpty(p.Account)).WithMessage("用户名长度必须再2-100个字符之间");
|
||||
//RuleFor(p => p.Account).EmailAddress().When(p => !p.Account.Contains("@") && !string.IsNullOrEmpty(p.Account)).WithMessage("电子邮件地址不正确");
|
||||
// helper funcs to get localized text or fallback
|
||||
string AccountEmpty() => localizer?["Login.Account.Empty"].Value ?? "登录账号不能为空";
|
||||
string AccountLength() => localizer?["Login.Account.Length"].Value ?? "用户名长度必须再2-100个字符之间";
|
||||
string PasswordEmpty() => localizer?["Login.Password.Empty"].Value ?? "请输入登录密码";
|
||||
string PasswordLength() => localizer?["Login.Password.Length"].Value ?? "登录密码必须在6-32位长度之间";
|
||||
|
||||
//RuleFor(p => p.Username).NotEmpty().WithMessage("用户名不能为空");
|
||||
//RuleFor(p => p.Username).Length(2, 50).When(p => !string.IsNullOrEmpty(p.Username)).WithMessage("用户名长度必须再2-50个字符之间");
|
||||
//RuleFor(p => p.Account).NotEmpty().WithMessage("电子邮件地址不能为空");
|
||||
//RuleFor(p => p.Email).EmailAddress().When(p => !string.IsNullOrEmpty(p.Email)).WithMessage(p => localizer["Form.Email.Invalid"]);
|
||||
//RuleFor(p => p.Email).MaximumLength(128).When(p => !string.IsNullOrEmpty(p.Email)).WithMessage(p => localizer["Form.Email.LengthInvalid"]);
|
||||
RuleFor(p => p.Password).NotEmpty().WithMessage("请输入登录密码");
|
||||
RuleFor(p => p.Password).Length(6, 32).When(p => !string.IsNullOrEmpty(p.Password)).WithMessage("登录密码必须在6-32位长度之间");
|
||||
//RuleFor(p => p.ConfirmPassword).NotEmpty().WithMessage(p => localizer["Form.ConfirmPassword.Empty"]);
|
||||
//RuleFor(p => p.ConfirmPassword).Equal(p => p.Password).When(p => !string.IsNullOrEmpty(p.Password) && !string.IsNullOrEmpty(p.ConfirmPassword)).WithMessage(p => localizer["Form.ConfirmPassword.Different"]);
|
||||
RuleFor(p => p.Account)
|
||||
.NotEmpty()
|
||||
.WithMessage(_ => AccountEmpty());
|
||||
|
||||
RuleFor(p => p.Account)
|
||||
.Length(2, 100)
|
||||
.When(p => !string.IsNullOrEmpty(p.Account))
|
||||
.WithMessage(_ => AccountLength());
|
||||
|
||||
RuleFor(p => p.Password)
|
||||
.NotEmpty()
|
||||
.WithMessage(_ => PasswordEmpty());
|
||||
|
||||
RuleFor(p => p.Password)
|
||||
.Length(6, 32)
|
||||
.When(p => !string.IsNullOrEmpty(p.Password))
|
||||
.WithMessage(_ => PasswordLength());
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Atomx.Admin.Client.Validators
|
||||
{
|
||||
public sealed class ValidatorsMarker
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
window.CookieReader = {
|
||||
window.cookies = {
|
||||
Read: function (name) {
|
||||
try {
|
||||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||
@@ -32,10 +32,3 @@ window.setHtmlLang = function (lang) {
|
||||
if (document && document.documentElement) document.documentElement.lang = lang || '';
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
// simple cookies wrapper used earlier as cookies.Write
|
||||
window.cookies = {
|
||||
Write: function (name, value, expiresIso) {
|
||||
return window.CookieReader.Write(name, value, expiresIso);
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,8 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Atomx.Admin.Controllers
|
||||
{
|
||||
@@ -43,6 +45,8 @@ namespace Atomx.Admin.Controllers
|
||||
private readonly JwtSetting _jwtSetting;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||
private readonly IValidator<LoginModel> _loginValidator;
|
||||
private readonly IStringLocalizer<SignController> _localizer;
|
||||
|
||||
public SignController(
|
||||
ILogger<SignController> logger,
|
||||
@@ -52,7 +56,9 @@ namespace Atomx.Admin.Controllers
|
||||
DataContext dbContext,
|
||||
JwtSetting jwtSetting,
|
||||
ICacheService cacheService,
|
||||
AuthenticationStateProvider authenticationStateProvider)
|
||||
AuthenticationStateProvider authenticationStateProvider,
|
||||
IValidator<LoginModel> loginValidator,
|
||||
IStringLocalizer<SignController> localizer)
|
||||
{
|
||||
_logger = logger;
|
||||
_identityService = identityService;
|
||||
@@ -62,6 +68,8 @@ namespace Atomx.Admin.Controllers
|
||||
_jwtSetting = jwtSetting;
|
||||
_cacheService = cacheService;
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
_loginValidator = loginValidator;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -73,8 +81,7 @@ namespace Atomx.Admin.Controllers
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||
{
|
||||
var validator = new LoginModelValidator();
|
||||
var validation = validator.Validate(model);
|
||||
var validation = await _loginValidator.ValidateAsync(model);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
var message = validation.Errors.FirstOrDefault()?.ErrorMessage ?? string.Empty;
|
||||
@@ -94,18 +101,19 @@ namespace Atomx.Admin.Controllers
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("用户不存在", null));
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail(_localizer["Sign.UserNotFound"], null));
|
||||
}
|
||||
|
||||
// 简单密码校验(项目 uses MD5 存储示例)
|
||||
if (user.Password != model.Password.ToMd5Password())
|
||||
{
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("账号密码不正确", null));
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail(_localizer["Sign.InvalidCredentials"], null));
|
||||
}
|
||||
|
||||
if (user.LockoutEndTime.HasValue && user.LockoutEndTime.Value > DateTime.UtcNow)
|
||||
{
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail($"账号已锁定,解锁时间:{user.LockoutEndTime.Value.ToLocalTime()}", null));
|
||||
var unlock = user.LockoutEndTime.Value.ToLocalTime();
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail(string.Format(_localizer["Sign.Locked"], unlock), null));
|
||||
}
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
@@ -171,9 +179,6 @@ namespace Atomx.Admin.Controllers
|
||||
var cacheKey = $"token:{HashToken(accessToken)}";
|
||||
await _cacheService.SetCacheAsync(cacheKey, user.Id, _jwtSetting.AccessTokenExpirationMinutes);
|
||||
|
||||
|
||||
|
||||
|
||||
var authResponse = new AuthResponse
|
||||
{
|
||||
Token = accessToken,
|
||||
@@ -218,17 +223,17 @@ namespace Atomx.Admin.Controllers
|
||||
var uid = _identityService.GetUserId();
|
||||
if (uid == 0)
|
||||
{
|
||||
return BadRequest(new ApiResult<AuthResponse>().IsFail("无效的令牌请求", null));
|
||||
return BadRequest(new ApiResult<AuthResponse>().IsFail(_localizer["Sign.InvalidTokenRequest"], null));
|
||||
}
|
||||
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken))
|
||||
{
|
||||
return BadRequest(new ApiResult<AuthResponse>().IsFail("无效的刷新请求", null));
|
||||
return BadRequest(new ApiResult<AuthResponse>().IsFail(_localizer["Sign.InvalidRefreshRequest"], null));
|
||||
}
|
||||
|
||||
var user = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == uid);
|
||||
if (user == null)
|
||||
throw new SecurityTokenException("用户不存在或已被禁用");
|
||||
throw new SecurityTokenException(_localizer["Sign.UserNotFound"].ToString());
|
||||
|
||||
try
|
||||
{
|
||||
@@ -242,7 +247,7 @@ namespace Atomx.Admin.Controllers
|
||||
!rt.IsRevoked);
|
||||
|
||||
if (storedToken == null)
|
||||
throw new SecurityTokenException("无效的刷新令牌");
|
||||
throw new SecurityTokenException(_localizer["Sign.InvalidRefreshToken"].ToString());
|
||||
|
||||
// 标记该 refresh token 为已撤销(一次性)
|
||||
storedToken.IsRevoked = true;
|
||||
@@ -333,12 +338,12 @@ namespace Atomx.Admin.Controllers
|
||||
catch (SecurityTokenException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "刷新失败:刷新令牌无效或 access token 无效");
|
||||
return Unauthorized(new ApiResult<AuthResponse>().IsFail("刷新令牌无效或已过期", null));
|
||||
return Unauthorized(new ApiResult<AuthResponse>().IsFail(_localizer["Sign.RefreshTokenInvalid"], null));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "刷新令牌时发生内部错误");
|
||||
return StatusCode(500, new ApiResult<AuthResponse>().IsFail("服务器内部错误", null));
|
||||
return StatusCode(500, new ApiResult<AuthResponse>().IsFail(_localizer["Sign.InternalServerError"], null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +364,7 @@ namespace Atomx.Admin.Controllers
|
||||
.FirstOrDefaultAsync(rt => rt.Token == hashedToken);
|
||||
|
||||
if (token == null || token.IsRevoked)
|
||||
return new JsonResult(new ApiResult<string>().IsSuccess("已退出"));
|
||||
return new JsonResult(new ApiResult<string>().IsSuccess(_localizer["Sign.LogoutSuccess"].ToString()));
|
||||
|
||||
token.IsRevoked = true;
|
||||
token.RevokedTime = DateTime.UtcNow;
|
||||
@@ -399,10 +404,9 @@ namespace Atomx.Admin.Controllers
|
||||
_logger.LogWarning(ex, "登出时清除 Cookie 失败(允许)");
|
||||
}
|
||||
|
||||
return new JsonResult(new ApiResult<string>().IsSuccess("已退出"));
|
||||
return new JsonResult(new ApiResult<string>().IsSuccess(_localizer["Sign.LogoutSuccess"].ToString()));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 生成随机 refresh token(明文,由服务返回到客户端,数据库仅存哈希)
|
||||
/// </summary>
|
||||
|
||||
@@ -11,6 +11,7 @@ using Atomx.Data;
|
||||
using Atomx.Data.Services;
|
||||
using Atomx.Utils.Json.Converts;
|
||||
using Blazored.LocalStorage;
|
||||
using FluentValidation;
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -149,6 +150,9 @@ builder.Services.AddOpenApi();
|
||||
builder.Services.AddAntDesign();
|
||||
builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("Monitoring"));
|
||||
|
||||
// ע<><D7A2> FluentValidation <20><>֤<EFBFBD><D6A4>
|
||||
builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Preload localization files on startup to ensure server-side rendering finds translations
|
||||
|
||||
@@ -15,5 +15,18 @@
|
||||
"weather.title": "Weather",
|
||||
"weather.summary": "A quick weather overview",
|
||||
"weather.temperature": "Temperature",
|
||||
"weather.refresh": "Refresh"
|
||||
"weather.refresh": "Refresh",
|
||||
"Login.Account.Empty": "Please enter your account",
|
||||
"Login.Account.Length": "Account length must be between 2 and 100 characters",
|
||||
"Login.Password.Empty": "Please enter your password",
|
||||
"Login.Password.Length": "Password length must be between 6 and 32 characters",
|
||||
"Sign.UserNotFound": "User not found",
|
||||
"Sign.InvalidCredentials": "Account or password is incorrect",
|
||||
"Sign.Locked": "Account locked, unlock time: {0}",
|
||||
"Sign.InvalidTokenRequest": "Invalid token request",
|
||||
"Sign.InvalidRefreshRequest": "Invalid refresh request",
|
||||
"Sign.InvalidRefreshToken": "Invalid refresh token",
|
||||
"Sign.RefreshTokenInvalid": "Refresh token invalid or expired",
|
||||
"Sign.InternalServerError": "Internal server error",
|
||||
"Sign.LogoutSuccess": "Logged out"
|
||||
}
|
||||
|
||||
@@ -15,5 +15,20 @@
|
||||
"weather.title": "天气",
|
||||
"weather.summary": "天气概览",
|
||||
"weather.temperature": "温度",
|
||||
"weather.refresh": "刷新"
|
||||
"weather.refresh": "刷新",
|
||||
|
||||
"Login.Account.Empty": "请输入登录账号",
|
||||
"Login.Account.Length": "用户名长度必须在2-100个字符之间",
|
||||
"Login.Password.Empty": "请输入登录密码",
|
||||
"Login.Password.Length": "登录密码必须在6-32位长度之间",
|
||||
|
||||
"Sign.UserNotFound": "用户不存在",
|
||||
"Sign.InvalidCredentials": "账号或密码不正确",
|
||||
"Sign.Locked": "账号已锁定,解锁时间:{0}",
|
||||
"Sign.InvalidTokenRequest": "无效的令牌请求",
|
||||
"Sign.InvalidRefreshRequest": "无效的刷新请求",
|
||||
"Sign.InvalidRefreshToken": "无效的刷新令牌",
|
||||
"Sign.RefreshTokenInvalid": "刷新令牌无效或已过期",
|
||||
"Sign.InternalServerError": "服务器内部错误",
|
||||
"Sign.LogoutSuccess": "已退出"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user