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