From 2318dff192d1345aea0e6108c9eaf79b2fc5d277 Mon Sep 17 00:00:00 2001 From: yxw <17074267@qq.com> Date: Tue, 9 Dec 2025 17:39:21 +0800 Subject: [PATCH] fix localization --- .../Atomx.Admin.Client.csproj | 1 + .../Pages/DebugLocalization.razor | 2 +- Atomx.Admin/Atomx.Admin.Client/Program.cs | 5 +++ .../Services/LocalizationProvider.cs | 2 +- .../Validators/LoginModelValidator.cs | 37 +++++++++++------ .../Validators/ValidatorsMarker.cs | 6 +++ .../Atomx.Admin.Client/wwwroot/js/common.js | 9 +---- .../Atomx.Admin/Controllers/SignController.cs | 40 ++++++++++--------- Atomx.Admin/Atomx.Admin/Program.cs | 4 ++ .../wwwroot/localization/en-US.json | 15 ++++++- .../wwwroot/localization/zh-Hans.json | 17 +++++++- 11 files changed, 95 insertions(+), 43 deletions(-) create mode 100644 Atomx.Admin/Atomx.Admin.Client/Validators/ValidatorsMarker.cs diff --git a/Atomx.Admin/Atomx.Admin.Client/Atomx.Admin.Client.csproj b/Atomx.Admin/Atomx.Admin.Client/Atomx.Admin.Client.csproj index 44da03a..d442b1b 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Atomx.Admin.Client.csproj +++ b/Atomx.Admin/Atomx.Admin.Client/Atomx.Admin.Client.csproj @@ -14,6 +14,7 @@ + diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor index 58f1813..49de35e 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/DebugLocalization.razor @@ -113,7 +113,7 @@ { try { - jsResult = await JS.InvokeAsync("CookieReader.Read", "atomx.culture"); + jsResult = await JS.InvokeAsync("cookies.Read", "atomx.culture"); } catch (Exception ex) { diff --git a/Atomx.Admin/Atomx.Admin.Client/Program.cs b/Atomx.Admin/Atomx.Admin.Client/Program.cs index 6ec603d..f83dfb8 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Program.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Program.cs @@ -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(sp => return new HttpService(httpClient, httpContextAccessor); }); +builder.Services.AddValidatorsFromAssembly(typeof(Atomx.Admin.Client.Validators.LoginModelValidator).Assembly); + builder.Services.AddAntDesign(); diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs index 19dec8f..f3938d8 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Services/LocalizationProvider.cs @@ -169,7 +169,7 @@ namespace Atomx.Admin.Client.Services { if (_jsRuntime != null && OperatingSystem.IsBrowser()) { - var cookieVal = await _jsRuntime.InvokeAsync("CookieReader.Read", CookieName); + var cookieVal = await _jsRuntime.InvokeAsync("cookies.Read", CookieName); _logger?.LogDebug("Cookie '{CookieName}'='{CookieVal}'", CookieName, cookieVal); if (!string.IsNullOrEmpty(cookieVal)) { diff --git a/Atomx.Admin/Atomx.Admin.Client/Validators/LoginModelValidator.cs b/Atomx.Admin/Atomx.Admin.Client/Validators/LoginModelValidator.cs index 2d91ac5..d3f4bd5 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Validators/LoginModelValidator.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Validators/LoginModelValidator.cs @@ -1,25 +1,36 @@ using Atomx.Admin.Client.Models; using FluentValidation; +using Microsoft.Extensions.Localization; namespace Atomx.Admin.Client.Validators { public class LoginModelValidator : AbstractValidator { - public LoginModelValidator() + public LoginModelValidator(IStringLocalizer 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>> ValidateValue => async (model, propertyName) => diff --git a/Atomx.Admin/Atomx.Admin.Client/Validators/ValidatorsMarker.cs b/Atomx.Admin/Atomx.Admin.Client/Validators/ValidatorsMarker.cs new file mode 100644 index 0000000..62ffc17 --- /dev/null +++ b/Atomx.Admin/Atomx.Admin.Client/Validators/ValidatorsMarker.cs @@ -0,0 +1,6 @@ +namespace Atomx.Admin.Client.Validators +{ + public sealed class ValidatorsMarker + { + } +} diff --git a/Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js b/Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js index 2be76a1..42e4be5 100644 --- a/Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js +++ b/Atomx.Admin/Atomx.Admin.Client/wwwroot/js/common.js @@ -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); - } -}; \ No newline at end of file diff --git a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs index 184965d..43ec2f1 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs @@ -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 _loginValidator; + private readonly IStringLocalizer _localizer; public SignController( ILogger logger, @@ -52,7 +56,9 @@ namespace Atomx.Admin.Controllers DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService, - AuthenticationStateProvider authenticationStateProvider) + AuthenticationStateProvider authenticationStateProvider, + IValidator loginValidator, + IStringLocalizer localizer) { _logger = logger; _identityService = identityService; @@ -62,6 +68,8 @@ namespace Atomx.Admin.Controllers _jwtSetting = jwtSetting; _cacheService = cacheService; _authenticationStateProvider = authenticationStateProvider; + _loginValidator = loginValidator; + _localizer = localizer; } /// @@ -73,8 +81,7 @@ namespace Atomx.Admin.Controllers [AllowAnonymous] public async Task 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().IsFail("用户不存在", null)); + return new JsonResult(new ApiResult().IsFail(_localizer["Sign.UserNotFound"], null)); } // 简单密码校验(项目 uses MD5 存储示例) if (user.Password != model.Password.ToMd5Password()) { - return new JsonResult(new ApiResult().IsFail("账号密码不正确", null)); + return new JsonResult(new ApiResult().IsFail(_localizer["Sign.InvalidCredentials"], null)); } if (user.LockoutEndTime.HasValue && user.LockoutEndTime.Value > DateTime.UtcNow) { - return new JsonResult(new ApiResult().IsFail($"账号已锁定,解锁时间:{user.LockoutEndTime.Value.ToLocalTime()}", null)); + var unlock = user.LockoutEndTime.Value.ToLocalTime(); + return new JsonResult(new ApiResult().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().IsFail("无效的令牌请求", null)); + return BadRequest(new ApiResult().IsFail(_localizer["Sign.InvalidTokenRequest"], null)); } if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken)) { - return BadRequest(new ApiResult().IsFail("无效的刷新请求", null)); + return BadRequest(new ApiResult().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().IsFail("刷新令牌无效或已过期", null)); + return Unauthorized(new ApiResult().IsFail(_localizer["Sign.RefreshTokenInvalid"], null)); } catch (Exception ex) { _logger.LogError(ex, "刷新令牌时发生内部错误"); - return StatusCode(500, new ApiResult().IsFail("服务器内部错误", null)); + return StatusCode(500, new ApiResult().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().IsSuccess("已退出")); + return new JsonResult(new ApiResult().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().IsSuccess("已退出")); + return new JsonResult(new ApiResult().IsSuccess(_localizer["Sign.LogoutSuccess"].ToString())); } - /// /// 生成随机 refresh token(明文,由服务返回到客户端,数据库仅存哈希) /// diff --git a/Atomx.Admin/Atomx.Admin/Program.cs b/Atomx.Admin/Atomx.Admin/Program.cs index 2f1bd4c..9b77014 100644 --- a/Atomx.Admin/Atomx.Admin/Program.cs +++ b/Atomx.Admin/Atomx.Admin/Program.cs @@ -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(builder.Configuration.GetSection("Monitoring")); +// ע FluentValidation ֤ +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 diff --git a/Atomx.Admin/Atomx.Admin/wwwroot/localization/en-US.json b/Atomx.Admin/Atomx.Admin/wwwroot/localization/en-US.json index 3723e3e..a7a039e 100644 --- a/Atomx.Admin/Atomx.Admin/wwwroot/localization/en-US.json +++ b/Atomx.Admin/Atomx.Admin/wwwroot/localization/en-US.json @@ -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" } diff --git a/Atomx.Admin/Atomx.Admin/wwwroot/localization/zh-Hans.json b/Atomx.Admin/Atomx.Admin/wwwroot/localization/zh-Hans.json index eeaab86..3dbf57f 100644 --- a/Atomx.Admin/Atomx.Admin/wwwroot/localization/zh-Hans.json +++ b/Atomx.Admin/Atomx.Admin/wwwroot/localization/zh-Hans.json @@ -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": "已退出" }