committed by
GitHub
77 changed files with 2005 additions and 655 deletions
@ -1,9 +1,27 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Account.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
[DependsOn(typeof(AbpAccountDomainSharedModule))] |
|||
[DependsOn( |
|||
typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule))] |
|||
public class AbpAccountApplicationContractsModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAccountApplicationContractsModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<AccountResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Account/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,35 +1,43 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class PhoneNumberRegisterDto |
|||
public class PhoneRegisterDto |
|||
{ |
|||
[Required] |
|||
[Phone] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] |
|||
[Display(Name = "PhoneNumber")] |
|||
public string PhoneNumber { get; set; } |
|||
|
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))] |
|||
[DisplayName("Name")] |
|||
public string Name { get; set; } |
|||
|
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] |
|||
[DisplayName("UserName")] |
|||
public string UserName { get; set; } |
|||
|
|||
[EmailAddress] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] |
|||
[DisplayName("EmailAddress")] |
|||
public string EmailAddress { get; set; } |
|||
|
|||
[Required] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
|||
[DataType(DataType.Password)] |
|||
[DisplayName("Password")] |
|||
[DisableAuditing] |
|||
public string Password { get; set; } |
|||
|
|||
[Required] |
|||
[StringLength(6)] |
|||
public string VerifyCode { get; set; } |
|||
[StringLength(6,MinimumLength = 6)] |
|||
[DisableAuditing] |
|||
[DisplayName("DisplayName:SmsVerifyCode")] |
|||
public string Code { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class SendPhoneResetPasswordCodeDto |
|||
{ |
|||
[Required] |
|||
[Phone] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] |
|||
[Display(Name = "PhoneNumber")] |
|||
public string PhoneNumber { get; set; } |
|||
} |
|||
} |
|||
@ -1,17 +1,45 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public interface IAccountAppService : IApplicationService |
|||
{ |
|||
Task<IdentityUserDto> RegisterAsync(PhoneNumberRegisterDto input); |
|||
|
|||
Task<IdentityUserDto> RegisterAsync(WeChatRegisterDto input); |
|||
|
|||
Task ResetPasswordAsync(PasswordResetDto input); |
|||
|
|||
Task VerifyPhoneNumberAsync(VerifyDto input); |
|||
/// <summary>
|
|||
/// 通过手机号注册用户账户
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task RegisterAsync(PhoneRegisterDto input); |
|||
/// <summary>
|
|||
/// 通过微信注册用户账户
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task RegisterAsync(WeChatRegisterDto input); |
|||
/// <summary>
|
|||
/// 通过手机号重置用户密码
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task ResetPasswordAsync(PhoneResetPasswordDto input); |
|||
/// <summary>
|
|||
/// 发送手机注册验证码短信
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input); |
|||
/// <summary>
|
|||
/// 发送手机登录验证码短信
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input); |
|||
/// <summary>
|
|||
/// 发送手机重置密码验证码短信
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"SendRepeatSmsVerifyCode": "Phone verification code cannot be sent repeatedly within {0} minutes!", |
|||
"DuplicatePhoneNumber": "The phone number already exists!", |
|||
"PhoneNumberNotRegisterd": "The registered mobile phone number is not registered!", |
|||
"InvalidSmsVerifyCode": "The phone verification code is invalid or expired!", |
|||
"RequiredEmailAddress": "Email address required", |
|||
"InvalidPhoneNumber": "Invalid phone number", |
|||
"DuplicateWeChat": "The wechat has been registered!", |
|||
"DisplayName:SmsVerifyCode": "SMS verification code", |
|||
"DisplayName:EmailVerifyCode": "Mail verification code", |
|||
"DisplayName:WeChatCode": "Wechat login code" |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"SendRepeatSmsVerifyCode": "手机验证码不能在 {0} 分钟内重复发送!", |
|||
"DuplicatePhoneNumber": "手机号已经存在!", |
|||
"PhoneNumberNotRegisterd": "手机号码未注册!", |
|||
"InvalidSmsVerifyCode": "手机验证码无效或已经过期!", |
|||
"RequiredEmailAddress": "邮件地址必须输入", |
|||
"InvalidPhoneNumber": "手机号无效", |
|||
"DuplicateWeChat": "微信号已经注册过!", |
|||
"DisplayName:SmsVerifyCode": "短信验证码", |
|||
"DisplayName:EmailVerifyCode": "邮件验证码", |
|||
"DisplayName:WeChatCode": "微信登录凭证" |
|||
} |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
using LINGYUN.Abp.Account.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Settings; |
|||
|
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class AccountSettingDefinitionProvider : SettingDefinitionProvider |
|||
{ |
|||
public override void Define(ISettingDefinitionContext context) |
|||
{ |
|||
|
|||
context.Add(GetAccountSettings()); |
|||
} |
|||
|
|||
protected SettingDefinition[] GetAccountSettings() |
|||
{ |
|||
return new SettingDefinition[] |
|||
{ |
|||
new SettingDefinition( |
|||
name: AccountSettingNames.SmsRegisterTemplateCode, |
|||
defaultValue: "SMS_190728520", |
|||
displayName: L("DisplayName:SmsRegisterTemplateCode"), |
|||
description: L("Description:SmsRegisterTemplateCode"), |
|||
isVisibleToClients: true) |
|||
.WithProviders( |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
name: AccountSettingNames.SmsSigninTemplateCode, |
|||
defaultValue: "SMS_190728516", |
|||
displayName: L("DisplayName:SmsSigninTemplateCode"), |
|||
description: L("Description:SmsSigninTemplateCode"), |
|||
isVisibleToClients: true) |
|||
.WithProviders( |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
name: AccountSettingNames.SmsResetPasswordTemplateCode, |
|||
defaultValue: "SMS_192530831", |
|||
displayName: L("DisplayName:SmsResetPasswordTemplateCode"), |
|||
description: L("Description:SmsResetPasswordTemplateCode"), |
|||
isVisibleToClients: true) |
|||
.WithProviders( |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
new SettingDefinition( |
|||
name: AccountSettingNames.PhoneVerifyCodeExpiration, |
|||
defaultValue: "3", |
|||
displayName: L("DisplayName:PhoneVerifyCodeExpiration"), |
|||
description: L("Description:PhoneVerifyCodeExpiration"), |
|||
isVisibleToClients: true) |
|||
.WithProviders( |
|||
GlobalSettingValueProvider.ProviderName, |
|||
TenantSettingValueProvider.ProviderName), |
|||
}; |
|||
} |
|||
|
|||
protected LocalizableString L(string name) |
|||
{ |
|||
return LocalizableString.Create<AccountResource>(name); |
|||
} |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
namespace LINGYUN.Abp.Account |
|||
{ |
|||
public class AccountSettingNames |
|||
{ |
|||
public const string GroupName = "Abp.Account"; |
|||
/// <summary>
|
|||
/// 短信验证码过期时间
|
|||
/// </summary>
|
|||
public const string PhoneVerifyCodeExpiration = GroupName + ".PhoneVerifyCodeExpiration"; |
|||
/// <summary>
|
|||
/// 用户注册短信验证码模板号
|
|||
/// </summary>
|
|||
public const string SmsRegisterTemplateCode = GroupName + ".SmsRegisterTemplateCode"; |
|||
/// <summary>
|
|||
/// 用户登录短信验证码模板号
|
|||
/// </summary>
|
|||
public const string SmsSigninTemplateCode = GroupName + ".SmsSigninTemplateCode"; |
|||
/// <summary>
|
|||
/// 用户重置密码短信验证码模板号
|
|||
/// </summary>
|
|||
public const string SmsResetPasswordTemplateCode = GroupName + ".SmsResetPasswordTemplateCode"; |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Account.Localization |
|||
{ |
|||
[LocalizationResourceName("LINYUNAbpAccount")] |
|||
public class AccountResource |
|||
{ |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"PhoneNumberNotRegisterd": "The registered mobile phone number is not registered!", |
|||
"PhoneVerifyCodeInvalid": "The phone verification code is invalid or expired!", |
|||
"PhoneVerifyCodeNotRepeatSend": "Phone verification code cannot be sent repeatedly within {0} minutes!", |
|||
"DisplayName:SmsRegisterTemplateCode": "Register sms template", |
|||
"Description:SmsRegisterTemplateCode": "When the user registers, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration", |
|||
"DisplayName:SmsSigninTemplateCode": "Signin sms template", |
|||
"Description:SmsSigninTemplateCode": "When the user logs in, he/she should send the template number of the SMS verification code and fill in the template number of the corresponding cloud platform registration", |
|||
"DisplayName:SmsResetPasswordTemplateCode": "Reset password sms template", |
|||
"Description:SmsResetPasswordTemplateCode": "When the user resets the password, he/she sends the template number of SMS verification code and fills in the template number registered on the cloud platform", |
|||
"DisplayName:PhoneVerifyCodeExpiration": "SMS verification code validity", |
|||
"Description:PhoneVerifyCodeExpiration": "The valid time for the user to send SMS verification code, unit m, default 3m", |
|||
"RequiredEmailAddress": "Email address required", |
|||
"InvalidPhoneNumber": "Invalid phone number", |
|||
"DuplicatePhoneNumber": "The phone number {0} has been registered!", |
|||
"DuplicateWeChat": "The wechat has been registered!" |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"PhoneNumberNotRegisterd": "手机号码未注册!", |
|||
"PhoneVerifyCodeInvalid": "手机验证码无效或已经过期!", |
|||
"PhoneVerifyCodeNotRepeatSend": "手机验证码不能在 {0} 分钟内重复发送!", |
|||
"DisplayName:SmsRegisterTemplateCode": "用户注册短信模板", |
|||
"Description:SmsRegisterTemplateCode": "用户注册时发送短信验证码的模板号,填写对应云平台注册的模板号", |
|||
"DisplayName:SmsSigninTemplateCode": "用户登录短信模板", |
|||
"Description:SmsSigninTemplateCode": "用户登录时发送短信验证码的模板号,填写对应云平台注册的模板号", |
|||
"DisplayName:SmsResetPasswordTemplateCode": "用户重置密码短信模板", |
|||
"Description:SmsResetPasswordTemplateCode": "用户重置密码时发送短信验证码的模板号,填写对应云平台注册的模板号", |
|||
"DisplayName:PhoneVerifyCodeExpiration": "短信验证码有效期", |
|||
"Description:PhoneVerifyCodeExpiration": "用户发送短信验证码的有效时长,单位m,默认3m", |
|||
"RequiredEmailAddress": "邮件地址必须输入", |
|||
"InvalidPhoneNumber": "手机号无效", |
|||
"DuplicatePhoneNumber": "手机号已经注册过!", |
|||
"DuplicateWeChat": "微信号已经注册过!" |
|||
} |
|||
} |
|||
@ -1,50 +0,0 @@ |
|||
using LINGYUN.Abp.Account.Localization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using IIdentityUserRepository = LINGYUN.Abp.Account.IIdentityUserRepository; |
|||
|
|||
namespace Microsoft.AspNetCore.Identity |
|||
{ |
|||
[Dependency(ServiceLifetime.Scoped, ReplaceServices = true)] |
|||
[ExposeServices(typeof(IUserValidator<IdentityUser>))] |
|||
public class PhoneNumberUserValidator : UserValidator<IdentityUser> |
|||
{ |
|||
private readonly IStringLocalizer<AccountResource> _stringLocalizer; |
|||
private readonly IIdentityUserRepository _identityUserRepository; |
|||
|
|||
public PhoneNumberUserValidator( |
|||
IStringLocalizer<AccountResource> stringLocalizer, |
|||
IIdentityUserRepository identityUserRepository) |
|||
{ |
|||
_stringLocalizer = stringLocalizer; |
|||
_identityUserRepository = identityUserRepository; |
|||
} |
|||
public override async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user) |
|||
{ |
|||
await ValidatePhoneNumberAsync(manager, user); |
|||
return await base.ValidateAsync(manager, user); |
|||
} |
|||
|
|||
protected virtual async Task ValidatePhoneNumberAsync(UserManager<IdentityUser> manager, IdentityUser user) |
|||
{ |
|||
var phoneNumber = await manager.GetPhoneNumberAsync(user); |
|||
if (phoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
return; |
|||
// 如果用户没有手机号,不验证
|
|||
//throw new UserFriendlyException(_stringLocalizer["InvalidPhoneNumber"].Value, "InvalidPhoneNumber");
|
|||
} |
|||
|
|||
var phoneNumberHasRegisted = await _identityUserRepository.PhoneNumberHasRegistedAsync(phoneNumber); |
|||
if (phoneNumberHasRegisted) |
|||
{ |
|||
throw new UserFriendlyException(_stringLocalizer["DuplicatePhoneNumber", phoneNumber].Value, "DuplicatePhoneNumber"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\AspNetCore\Mvc\Validation\Localization\MissingFields\en.json" /> |
|||
<None Remove="LINGYUN\Abp\AspNetCore\Mvc\Validation\Localization\MissingFields\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\AspNetCore\Mvc\Validation\Localization\MissingFields\en.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\AspNetCore\Mvc\Validation\Localization\MissingFields\zh-Hans.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="3.3.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Validation.Localization; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Validation |
|||
{ |
|||
[Obsolete("用于测试模型绑定与验证相关的类,无需引用")] |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcModule))] |
|||
public class AbpAspNetCoreMvcValidationModule : AbpModule |
|||
{ |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAspNetCoreMvcValidationModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<AbpValidationResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/AspNetCore/Mvc/Validation/Localization/MissingFields"); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,563 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.DataAnnotations; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; |
|||
using Microsoft.Extensions.Localization; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
|
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Validation |
|||
{ |
|||
public class AbpDataAnnotationsMetadataProvider : |
|||
IBindingMetadataProvider, |
|||
IDisplayMetadataProvider, |
|||
IValidationMetadataProvider |
|||
{ |
|||
// The [Nullable] attribute is synthesized by the compiler. It's best to just compare the type name.
|
|||
private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; |
|||
private const string NullableFlagsFieldName = "NullableFlags"; |
|||
|
|||
private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute"; |
|||
private const string NullableContextFlagsFieldName = "Flag"; |
|||
|
|||
private readonly IStringLocalizerFactory _stringLocalizerFactory; |
|||
private readonly MvcOptions _options; |
|||
private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions; |
|||
|
|||
public AbpDataAnnotationsMetadataProvider( |
|||
MvcOptions options, |
|||
IOptions<MvcDataAnnotationsLocalizationOptions> localizationOptions, |
|||
IStringLocalizerFactory stringLocalizerFactory) |
|||
{ |
|||
if (options == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
if (localizationOptions == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(localizationOptions)); |
|||
} |
|||
|
|||
_options = options; |
|||
_localizationOptions = localizationOptions.Value; |
|||
_stringLocalizerFactory = stringLocalizerFactory; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void CreateBindingMetadata(BindingMetadataProviderContext context) |
|||
{ |
|||
if (context == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
var editableAttribute = context.Attributes.OfType<EditableAttribute>().FirstOrDefault(); |
|||
if (editableAttribute != null) |
|||
{ |
|||
context.BindingMetadata.IsReadOnly = !editableAttribute.AllowEdit; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void CreateDisplayMetadata(DisplayMetadataProviderContext context) |
|||
{ |
|||
if (context == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
var attributes = context.Attributes; |
|||
var dataTypeAttribute = attributes.OfType<DataTypeAttribute>().FirstOrDefault(); |
|||
var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault(); |
|||
var displayColumnAttribute = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault(); |
|||
var displayFormatAttribute = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault(); |
|||
var displayNameAttribute = attributes.OfType<DisplayNameAttribute>().FirstOrDefault(); |
|||
var hiddenInputAttribute = attributes.OfType<HiddenInputAttribute>().FirstOrDefault(); |
|||
var scaffoldColumnAttribute = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault(); |
|||
var uiHintAttribute = attributes.OfType<UIHintAttribute>().FirstOrDefault(); |
|||
|
|||
// Special case the [DisplayFormat] attribute hanging off an applied [DataType] attribute. This property is
|
|||
// non-null for DataType.Currency, DataType.Date, DataType.Time, and potentially custom [DataType]
|
|||
// subclasses. The DataType.Currency, DataType.Date, and DataType.Time [DisplayFormat] attributes have a
|
|||
// non-null DataFormatString and the DataType.Date and DataType.Time [DisplayFormat] attributes have
|
|||
// ApplyFormatInEditMode==true.
|
|||
if (displayFormatAttribute == null && dataTypeAttribute != null) |
|||
{ |
|||
displayFormatAttribute = dataTypeAttribute.DisplayFormat; |
|||
} |
|||
|
|||
var displayMetadata = context.DisplayMetadata; |
|||
|
|||
// ConvertEmptyStringToNull
|
|||
if (displayFormatAttribute != null) |
|||
{ |
|||
displayMetadata.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull; |
|||
} |
|||
|
|||
// DataTypeName
|
|||
if (dataTypeAttribute != null) |
|||
{ |
|||
displayMetadata.DataTypeName = dataTypeAttribute.GetDataTypeName(); |
|||
} |
|||
else if (displayFormatAttribute != null && !displayFormatAttribute.HtmlEncode) |
|||
{ |
|||
displayMetadata.DataTypeName = nameof(DataType.Html); |
|||
} |
|||
|
|||
var containerType = context.Key.ContainerType ?? context.Key.ModelType; |
|||
IStringLocalizer localizer = null; |
|||
if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) |
|||
{ |
|||
localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory); |
|||
} |
|||
|
|||
// Description
|
|||
if (displayAttribute != null) |
|||
{ |
|||
if (localizer != null && |
|||
!string.IsNullOrEmpty(displayAttribute.Description) && |
|||
displayAttribute.ResourceType == null) |
|||
{ |
|||
displayMetadata.Description = () => localizer[displayAttribute.Description]; |
|||
} |
|||
else |
|||
{ |
|||
displayMetadata.Description = () => displayAttribute.GetDescription(); |
|||
} |
|||
} |
|||
|
|||
// DisplayFormatString
|
|||
if (displayFormatAttribute != null) |
|||
{ |
|||
displayMetadata.DisplayFormatString = displayFormatAttribute.DataFormatString; |
|||
} |
|||
|
|||
// DisplayName
|
|||
// DisplayAttribute has precedence over DisplayNameAttribute.
|
|||
if (displayAttribute?.GetName() != null) |
|||
{ |
|||
if (localizer != null && |
|||
!string.IsNullOrEmpty(displayAttribute.Name) && |
|||
displayAttribute.ResourceType == null) |
|||
{ |
|||
var displayName = localizer[displayAttribute.Name]; |
|||
displayMetadata.DisplayName = () => displayName; |
|||
} |
|||
else |
|||
{ |
|||
displayMetadata.DisplayName = () => displayAttribute.GetName(); |
|||
} |
|||
} |
|||
else if (displayNameAttribute != null) |
|||
{ |
|||
if (localizer != null && |
|||
!string.IsNullOrEmpty(displayNameAttribute.DisplayName)) |
|||
{ |
|||
displayMetadata.DisplayName = () => localizer[displayNameAttribute.DisplayName]; |
|||
} |
|||
else |
|||
{ |
|||
displayMetadata.DisplayName = () => displayNameAttribute.DisplayName; |
|||
} |
|||
} |
|||
|
|||
// EditFormatString
|
|||
if (displayFormatAttribute != null && displayFormatAttribute.ApplyFormatInEditMode) |
|||
{ |
|||
displayMetadata.EditFormatString = displayFormatAttribute.DataFormatString; |
|||
} |
|||
|
|||
// IsEnum et cetera
|
|||
var underlyingType = Nullable.GetUnderlyingType(context.Key.ModelType) ?? context.Key.ModelType; |
|||
var underlyingTypeInfo = underlyingType.GetTypeInfo(); |
|||
|
|||
if (underlyingTypeInfo.IsEnum) |
|||
{ |
|||
// IsEnum
|
|||
displayMetadata.IsEnum = true; |
|||
|
|||
// IsFlagsEnum
|
|||
displayMetadata.IsFlagsEnum = underlyingTypeInfo.IsDefined(typeof(FlagsAttribute), inherit: false); |
|||
|
|||
// EnumDisplayNamesAndValues and EnumNamesAndValues
|
|||
//
|
|||
// Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames().
|
|||
// That method orders by absolute value, then its behavior is undefined (but hopefully stable).
|
|||
// Add to EnumNamesAndValues in same order but Dictionary does not guarantee order will be preserved.
|
|||
|
|||
var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>(); |
|||
var namesAndValues = new Dictionary<string, string>(); |
|||
|
|||
IStringLocalizer enumLocalizer = null; |
|||
if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) |
|||
{ |
|||
enumLocalizer = _localizationOptions.DataAnnotationLocalizerProvider(underlyingType, _stringLocalizerFactory); |
|||
} |
|||
|
|||
var enumFields = Enum.GetNames(underlyingType) |
|||
.Select(name => underlyingType.GetField(name)) |
|||
.OrderBy(field => field.GetCustomAttribute<DisplayAttribute>(inherit: false)?.GetOrder() ?? 1000); |
|||
|
|||
foreach (var field in enumFields) |
|||
{ |
|||
var groupName = GetDisplayGroup(field); |
|||
var value = ((Enum)field.GetValue(obj: null)).ToString("d"); |
|||
|
|||
groupedDisplayNamesAndValues.Add(new KeyValuePair<EnumGroupAndName, string>( |
|||
new EnumGroupAndName( |
|||
groupName, |
|||
() => GetDisplayName(field, enumLocalizer)), |
|||
value)); |
|||
namesAndValues.Add(field.Name, value); |
|||
} |
|||
|
|||
displayMetadata.EnumGroupedDisplayNamesAndValues = groupedDisplayNamesAndValues; |
|||
displayMetadata.EnumNamesAndValues = namesAndValues; |
|||
} |
|||
|
|||
// HasNonDefaultEditFormat
|
|||
if (!string.IsNullOrEmpty(displayFormatAttribute?.DataFormatString) && |
|||
displayFormatAttribute?.ApplyFormatInEditMode == true) |
|||
{ |
|||
// Have a non-empty EditFormatString based on [DisplayFormat] from our cache.
|
|||
if (dataTypeAttribute == null) |
|||
{ |
|||
// Attributes include no [DataType]; [DisplayFormat] was applied directly.
|
|||
displayMetadata.HasNonDefaultEditFormat = true; |
|||
} |
|||
else if (dataTypeAttribute.DisplayFormat != displayFormatAttribute) |
|||
{ |
|||
// Attributes include separate [DataType] and [DisplayFormat]; [DisplayFormat] provided override.
|
|||
displayMetadata.HasNonDefaultEditFormat = true; |
|||
} |
|||
else if (dataTypeAttribute.GetType() != typeof(DataTypeAttribute)) |
|||
{ |
|||
// Attributes include [DisplayFormat] copied from [DataType] and [DataType] was of a subclass.
|
|||
// Assume the [DataType] constructor used the protected DisplayFormat setter to override its
|
|||
// default. That is derived [DataType] provided override.
|
|||
displayMetadata.HasNonDefaultEditFormat = true; |
|||
} |
|||
} |
|||
|
|||
// HideSurroundingHtml
|
|||
if (hiddenInputAttribute != null) |
|||
{ |
|||
displayMetadata.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue; |
|||
} |
|||
|
|||
// HtmlEncode
|
|||
if (displayFormatAttribute != null) |
|||
{ |
|||
displayMetadata.HtmlEncode = displayFormatAttribute.HtmlEncode; |
|||
} |
|||
|
|||
// NullDisplayText
|
|||
if (displayFormatAttribute != null) |
|||
{ |
|||
displayMetadata.NullDisplayText = displayFormatAttribute.NullDisplayText; |
|||
} |
|||
|
|||
// Order
|
|||
if (displayAttribute?.GetOrder() != null) |
|||
{ |
|||
displayMetadata.Order = displayAttribute.GetOrder().Value; |
|||
} |
|||
|
|||
// Placeholder
|
|||
if (displayAttribute != null) |
|||
{ |
|||
if (localizer != null && |
|||
!string.IsNullOrEmpty(displayAttribute.Prompt) && |
|||
displayAttribute.ResourceType == null) |
|||
{ |
|||
displayMetadata.Placeholder = () => localizer[displayAttribute.Prompt]; |
|||
} |
|||
else |
|||
{ |
|||
displayMetadata.Placeholder = () => displayAttribute.GetPrompt(); |
|||
} |
|||
} |
|||
|
|||
// ShowForDisplay
|
|||
if (scaffoldColumnAttribute != null) |
|||
{ |
|||
displayMetadata.ShowForDisplay = scaffoldColumnAttribute.Scaffold; |
|||
} |
|||
|
|||
// ShowForEdit
|
|||
if (scaffoldColumnAttribute != null) |
|||
{ |
|||
displayMetadata.ShowForEdit = scaffoldColumnAttribute.Scaffold; |
|||
} |
|||
|
|||
// SimpleDisplayProperty
|
|||
if (displayColumnAttribute != null) |
|||
{ |
|||
displayMetadata.SimpleDisplayProperty = displayColumnAttribute.DisplayColumn; |
|||
} |
|||
|
|||
// TemplateHint
|
|||
if (uiHintAttribute != null) |
|||
{ |
|||
displayMetadata.TemplateHint = uiHintAttribute.UIHint; |
|||
} |
|||
else if (hiddenInputAttribute != null) |
|||
{ |
|||
displayMetadata.TemplateHint = "HiddenInput"; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void CreateValidationMetadata(ValidationMetadataProviderContext context) |
|||
{ |
|||
if (context == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
// Read interface .Count once rather than per iteration
|
|||
var contextAttributes = context.Attributes; |
|||
var contextAttributesCount = contextAttributes.Count; |
|||
var attributes = new List<object>(contextAttributesCount); |
|||
|
|||
for (var i = 0; i < contextAttributesCount; i++) |
|||
{ |
|||
var attribute = contextAttributes[i]; |
|||
if (attribute is ValidationProviderAttribute validationProviderAttribute) |
|||
{ |
|||
attributes.AddRange(validationProviderAttribute.GetValidationAttributes()); |
|||
} |
|||
else |
|||
{ |
|||
attributes.Add(attribute); |
|||
} |
|||
} |
|||
|
|||
// RequiredAttribute marks a property as required by validation - this means that it
|
|||
// must have a non-null value on the model during validation.
|
|||
var requiredAttribute = attributes.OfType<RequiredAttribute>().FirstOrDefault(); |
|||
|
|||
// For non-nullable reference types, treat them as-if they had an implicit [Required].
|
|||
// This allows the developer to specify [Required] to customize the error message, so
|
|||
// if they already have [Required] then there's no need for us to do this check.
|
|||
if (!_options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes && |
|||
requiredAttribute == null && |
|||
!context.Key.ModelType.IsValueType && |
|||
context.Key.MetadataKind != ModelMetadataKind.Type) |
|||
{ |
|||
var addInferredRequiredAttribute = false; |
|||
if (context.Key.MetadataKind == ModelMetadataKind.Type) |
|||
{ |
|||
// Do nothing.
|
|||
} |
|||
else if (context.Key.MetadataKind == ModelMetadataKind.Property) |
|||
{ |
|||
var property = context.Key.PropertyInfo; |
|||
if (property is null) |
|||
{ |
|||
// PropertyInfo was unavailable on ModelIdentity prior to 3.1.
|
|||
// Making a cogent argument about the nullability of the property requires inspecting the declared type,
|
|||
// since looking at the runtime type may result in false positives: https://github.com/dotnet/aspnetcore/issues/14812
|
|||
// The only way we could arrive here is if the ModelMetadata was constructed using the non-default provider.
|
|||
// We'll cursorily examine the attributes on the property, but not the ContainerType to make a decision about it's nullability.
|
|||
|
|||
if (HasNullableAttribute(context.PropertyAttributes, out var propertyHasNullableAttribute)) |
|||
{ |
|||
addInferredRequiredAttribute = propertyHasNullableAttribute; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
addInferredRequiredAttribute = IsNullableReferenceType( |
|||
property.DeclaringType, |
|||
member: null, |
|||
context.PropertyAttributes); |
|||
} |
|||
} |
|||
else if (context.Key.MetadataKind == ModelMetadataKind.Parameter) |
|||
{ |
|||
addInferredRequiredAttribute = IsNullableReferenceType( |
|||
context.Key.ParameterInfo?.Member.ReflectedType, |
|||
context.Key.ParameterInfo.Member, |
|||
context.ParameterAttributes); |
|||
} |
|||
else |
|||
{ |
|||
throw new InvalidOperationException("Unsupported ModelMetadataKind: " + context.Key.MetadataKind); |
|||
} |
|||
|
|||
if (addInferredRequiredAttribute) |
|||
{ |
|||
// Since this behavior specifically relates to non-null-ness, we will use the non-default
|
|||
// option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
|
|||
// a validation error by default because we convert empty/whitespace strings to null
|
|||
// unless you say otherwise.
|
|||
requiredAttribute = new RequiredAttribute() |
|||
{ |
|||
AllowEmptyStrings = true, |
|||
}; |
|||
attributes.Add(requiredAttribute); |
|||
} |
|||
} |
|||
|
|||
if (requiredAttribute != null) |
|||
{ |
|||
context.ValidationMetadata.IsRequired = true; |
|||
} |
|||
|
|||
foreach (var attribute in attributes.OfType<ValidationAttribute>()) |
|||
{ |
|||
// If another provider has already added this attribute, do not repeat it.
|
|||
// This will prevent attributes like RemoteAttribute (which implement ValidationAttribute and
|
|||
// IClientModelValidator) to be added to the ValidationMetadata twice.
|
|||
// This is to ensure we do not end up with duplication validation rules on the client side.
|
|||
if (!context.ValidationMetadata.ValidatorMetadata.Contains(attribute)) |
|||
{ |
|||
context.ValidationMetadata.ValidatorMetadata.Add(attribute); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static string GetDisplayName(FieldInfo field, IStringLocalizer stringLocalizer) |
|||
{ |
|||
var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false); |
|||
if (display != null) |
|||
{ |
|||
// Note [Display(Name = "")] is allowed but we will not attempt to localize the empty name.
|
|||
var name = display.GetName(); |
|||
if (stringLocalizer != null && !string.IsNullOrEmpty(name) && display.ResourceType == null) |
|||
{ |
|||
name = stringLocalizer[name]; |
|||
} |
|||
|
|||
return name ?? field.Name; |
|||
} |
|||
|
|||
return field.Name; |
|||
} |
|||
|
|||
// Return non-empty group specified in a [Display] attribute for a field, if any; string.Empty otherwise.
|
|||
private static string GetDisplayGroup(FieldInfo field) |
|||
{ |
|||
var display = field.GetCustomAttribute<DisplayAttribute>(inherit: false); |
|||
if (display != null) |
|||
{ |
|||
// Note [Display(Group = "")] is allowed.
|
|||
var group = display.GetGroupName(); |
|||
if (group != null) |
|||
{ |
|||
return group; |
|||
} |
|||
} |
|||
|
|||
return string.Empty; |
|||
} |
|||
|
|||
internal static bool IsNullableReferenceType(Type containingType, MemberInfo member, IEnumerable<object> attributes) |
|||
{ |
|||
if (HasNullableAttribute(attributes, out var result)) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
return IsNullableBasedOnContext(containingType, member); |
|||
} |
|||
|
|||
// Internal for testing
|
|||
internal static bool HasNullableAttribute(IEnumerable<object> attributes, out bool isNullable) |
|||
{ |
|||
// [Nullable] is compiler synthesized, comparing by name.
|
|||
var nullableAttribute = attributes |
|||
.FirstOrDefault(a => string.Equals(a.GetType().FullName, NullableAttributeFullTypeName, StringComparison.Ordinal)); |
|||
if (nullableAttribute == null) |
|||
{ |
|||
isNullable = false; |
|||
return false; // [Nullable] not found
|
|||
} |
|||
|
|||
// We don't handle cases where generics and NNRT are used. This runs into a
|
|||
// fundamental limitation of ModelMetadata - we use a single Type and Property/Parameter
|
|||
// to look up the metadata. However when generics are involved and NNRT is in use
|
|||
// the distance between the [Nullable] and member we're looking at is potentially
|
|||
// unbounded.
|
|||
//
|
|||
// See: https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md#annotations
|
|||
if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field && |
|||
field.GetValue(nullableAttribute) is byte[] flags && |
|||
flags.Length > 0 && |
|||
flags[0] == 1) // First element is the property/parameter type.
|
|||
{ |
|||
isNullable = true; |
|||
return true; // [Nullable] found and type is an NNRT
|
|||
} |
|||
|
|||
isNullable = false; |
|||
return true; // [Nullable] found but type is not an NNRT
|
|||
} |
|||
|
|||
internal static bool IsNullableBasedOnContext(Type containingType, MemberInfo member) |
|||
{ |
|||
// For generic types, inspecting the nullability requirement additionally requires
|
|||
// inspecting the nullability constraint on generic type parameters. This is fairly non-triviial
|
|||
// so we'll just avoid calculating it. Users should still be able to apply an explicit [Required]
|
|||
// attribute on these members.
|
|||
if (containingType.IsGenericType) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// The [Nullable] and [NullableContext] attributes are not inherited.
|
|||
//
|
|||
// The [NullableContext] attribute can appear on a method or on the module.
|
|||
var attributes = member?.GetCustomAttributes(inherit: false) ?? Array.Empty<object>(); |
|||
var isNullable = AttributesHasNullableContext(attributes); |
|||
if (isNullable != null) |
|||
{ |
|||
return isNullable.Value; |
|||
} |
|||
|
|||
// Check on the containing type
|
|||
var type = containingType; |
|||
do |
|||
{ |
|||
attributes = type.GetCustomAttributes(inherit: false); |
|||
isNullable = AttributesHasNullableContext(attributes); |
|||
if (isNullable != null) |
|||
{ |
|||
return isNullable.Value; |
|||
} |
|||
|
|||
type = type.DeclaringType; |
|||
} |
|||
while (type != null); |
|||
|
|||
// If we don't find the attribute on the declaring type then repeat at the module level
|
|||
attributes = containingType.Module.GetCustomAttributes(inherit: false); |
|||
isNullable = AttributesHasNullableContext(attributes); |
|||
return isNullable ?? false; |
|||
|
|||
bool? AttributesHasNullableContext(object[] attributes) |
|||
{ |
|||
var nullableContextAttribute = attributes |
|||
.FirstOrDefault(a => string.Equals(a.GetType().FullName, NullableContextAttributeFullName, StringComparison.Ordinal)); |
|||
if (nullableContextAttribute != null) |
|||
{ |
|||
if (nullableContextAttribute.GetType().GetField(NullableContextFlagsFieldName) is FieldInfo field && |
|||
field.GetValue(nullableContextAttribute) is byte @byte) |
|||
{ |
|||
return @byte == 1; // [NullableContext] found
|
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using Microsoft.Extensions.Options; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Volo.Abp.AspNetCore.Mvc.Validation; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Validation.Localization; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Validation |
|||
{ |
|||
//[Dependency(ServiceLifetime.Singleton, ReplaceServices = true)]
|
|||
//[ExposeServices(typeof(IModelMetadataProvider))]
|
|||
public class AbpLocalizerModelMetadataProvider : DefaultModelMetadataProvider |
|||
{ |
|||
protected IStringLocalizer StringLocalizer { get; } |
|||
public AbpLocalizerModelMetadataProvider( |
|||
ICompositeMetadataDetailsProvider detailsProvider, |
|||
IStringLocalizer<AbpValidationResource> stringLocalizer) |
|||
: base(detailsProvider) |
|||
{ |
|||
StringLocalizer = stringLocalizer; |
|||
} |
|||
|
|||
public AbpLocalizerModelMetadataProvider( |
|||
IStringLocalizer<AbpValidationResource> stringLocalizer, |
|||
ICompositeMetadataDetailsProvider detailsProvider, |
|||
IOptions<MvcOptions> optionsAccessor) |
|||
: base(detailsProvider, optionsAccessor) |
|||
{ |
|||
StringLocalizer = stringLocalizer; |
|||
} |
|||
|
|||
protected override DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key) |
|||
{ |
|||
var details = base.CreatePropertyDetails(key); |
|||
|
|||
foreach (var detail in details) |
|||
{ |
|||
NormalizeMetadataDetail(detail); |
|||
} |
|||
|
|||
return details; |
|||
} |
|||
|
|||
private void NormalizeMetadataDetail(DefaultMetadataDetails detail) |
|||
{ |
|||
foreach (var validationAttribute in detail.ModelAttributes.Attributes.OfType<ValidationAttribute>()) |
|||
{ |
|||
NormalizeValidationAttrbute(validationAttribute, detail.DisplayMetadata?.DisplayFormatString ?? detail.Key.Name); |
|||
} |
|||
} |
|||
|
|||
protected virtual void NormalizeValidationAttrbute(ValidationAttribute validationAttribute, string displayName) |
|||
{ |
|||
if (validationAttribute.ErrorMessage == null) |
|||
{ |
|||
if (validationAttribute is RequiredAttribute required) |
|||
{ |
|||
validationAttribute.ErrorMessage = StringLocalizer["The field {0} is invalid.", displayName]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using Microsoft.AspNetCore.Mvc.DataAnnotations; |
|||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
|
|||
namespace LINGYUN.Abp.AspNetCore.Mvc.Validation |
|||
{ |
|||
public class DataAnnotationAutoLocalizationMetadataDetailsProvider : IDisplayMetadataProvider |
|||
{ |
|||
private const string PropertyLocalizationKeyPrefix = "DisplayName:"; |
|||
|
|||
private readonly Lazy<IStringLocalizerFactory> _stringLocalizerFactory; |
|||
private readonly Lazy<IOptions<MvcDataAnnotationsLocalizationOptions>> _localizationOptions; |
|||
|
|||
public DataAnnotationAutoLocalizationMetadataDetailsProvider(IServiceCollection services) |
|||
{ |
|||
_stringLocalizerFactory = services.GetRequiredServiceLazy<IStringLocalizerFactory>(); |
|||
_localizationOptions = services.GetRequiredServiceLazy<IOptions<MvcDataAnnotationsLocalizationOptions>>(); |
|||
} |
|||
|
|||
public void CreateDisplayMetadata(DisplayMetadataProviderContext context) |
|||
{ |
|||
var displayMetadata = context.DisplayMetadata; |
|||
if (displayMetadata.DisplayName != null) |
|||
{ |
|||
var displayName = displayMetadata.DisplayName(); |
|||
return; |
|||
} |
|||
|
|||
var attributes = context.Attributes; |
|||
|
|||
if (attributes.OfType<DisplayAttribute>().Any() || |
|||
attributes.OfType<DisplayNameAttribute>().Any()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (context.Key.Name.IsNullOrWhiteSpace()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_localizationOptions.Value.Value.DataAnnotationLocalizerProvider == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var containerType = context.Key.ContainerType ?? context.Key.ModelType; |
|||
var localizer = _localizationOptions.Value.Value.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory.Value); |
|||
|
|||
displayMetadata.DisplayName = () => |
|||
{ |
|||
var localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.Name]; |
|||
|
|||
if (localizedString.ResourceNotFound) |
|||
{ |
|||
localizedString = localizer[context.Key.Name]; |
|||
} |
|||
|
|||
return localizedString; |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"ThisFieldMustBeANumber": "The field must be a number!", |
|||
"The field {0} must be a number.": "The supplied value {0} must be a number!" |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"ThisFieldMustBeANumber": "字段必须是数字!", |
|||
"The field {0} must be a number.": "提供的值 {0} 必须是数字!" |
|||
} |
|||
} |
|||
@ -1,6 +1,6 @@ |
|||
namespace LINGYUN.Abp.Identity |
|||
{ |
|||
public class IdentityUserTwoFactorEnabledDto |
|||
public class ChangeTwoFactorEnabledDto |
|||
{ |
|||
public bool Enabled { get; set; } |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.Identity |
|||
{ |
|||
public class SendChangeEmailAddressCodeDto |
|||
{ |
|||
/// <summary>
|
|||
/// 新邮件地址
|
|||
/// </summary>
|
|||
[Required] |
|||
[EmailAddress] |
|||
[Display(Name = "EmailAddress")] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] |
|||
public string NewEmailAddress { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace LINGYUN.Abp.Identity |
|||
{ |
|||
public class SendChangePhoneNumberCodeDto |
|||
{ |
|||
/// <summary>
|
|||
/// 新手机号
|
|||
/// </summary>
|
|||
[Required] |
|||
[Phone] |
|||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] |
|||
[Display(Name = "PhoneNumber")] |
|||
public string NewPhoneNumber { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Identity |
|||
{ |
|||
public interface IUserSecurityCodeSender |
|||
{ |
|||
Task SendPhoneConfirmedCodeAsync( |
|||
string phone, |
|||
string token, |
|||
string template, // 传递模板号
|
|||
CancellationToken cancellation = default); |
|||
|
|||
Task SendEmailConfirmedCodeAsync( |
|||
string userName, |
|||
string email, |
|||
string token, |
|||
CancellationToken cancellation = default); |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Net; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.Identity.Security |
|||
{ |
|||
/// <summary>
|
|||
/// 微软的实现
|
|||
/// See: Microsoft.AspNetCore.Identity.Rfc6238AuthenticationService
|
|||
/// </summary>
|
|||
internal class DefaultTotpService : ITotpService, ISingletonDependency |
|||
{ |
|||
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); |
|||
private static readonly Encoding _encoding = new UTF8Encoding(false, true); |
|||
#if NETSTANDARD2_0
|
|||
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); |
|||
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); |
|||
#endif
|
|||
|
|||
// Generates a new 80-bit security token
|
|||
public static byte[] GenerateRandomKey() |
|||
{ |
|||
byte[] bytes = new byte[20]; |
|||
#if NETSTANDARD2_0
|
|||
_rng.GetBytes(bytes); |
|||
#else
|
|||
RandomNumberGenerator.Fill(bytes); |
|||
#endif
|
|||
return bytes; |
|||
} |
|||
|
|||
internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) |
|||
{ |
|||
// # of 0's = length of pin
|
|||
const int Mod = 1000000; |
|||
|
|||
// See https://tools.ietf.org/html/rfc4226
|
|||
// We can add an optional modifier
|
|||
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); |
|||
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); |
|||
|
|||
// Generate DT string
|
|||
var offset = hash[hash.Length - 1] & 0xf; |
|||
Debug.Assert(offset + 4 < hash.Length); |
|||
var binaryCode = (hash[offset] & 0x7f) << 24 |
|||
| (hash[offset + 1] & 0xff) << 16 |
|||
| (hash[offset + 2] & 0xff) << 8 |
|||
| (hash[offset + 3] & 0xff); |
|||
|
|||
return binaryCode % Mod; |
|||
} |
|||
|
|||
private static byte[] ApplyModifier(byte[] input, string modifier) |
|||
{ |
|||
if (String.IsNullOrEmpty(modifier)) |
|||
{ |
|||
return input; |
|||
} |
|||
|
|||
var modifierBytes = _encoding.GetBytes(modifier); |
|||
var combined = new byte[checked(input.Length + modifierBytes.Length)]; |
|||
Buffer.BlockCopy(input, 0, combined, 0, input.Length); |
|||
Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); |
|||
return combined; |
|||
} |
|||
|
|||
// More info: https://tools.ietf.org/html/rfc6238#section-4
|
|||
private static ulong GetCurrentTimeStepNumber() |
|||
{ |
|||
#if NETSTANDARD2_0
|
|||
var delta = DateTime.UtcNow - _unixEpoch; |
|||
#else
|
|||
var delta = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch; |
|||
#endif
|
|||
return (ulong)(delta.Ticks / _timestep.Ticks); |
|||
} |
|||
|
|||
public int GenerateCode(byte[] securityToken, string modifier = null) |
|||
{ |
|||
if (securityToken == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(securityToken)); |
|||
} |
|||
|
|||
// Allow a variance of no greater than 9 minutes in either direction
|
|||
var currentTimeStep = GetCurrentTimeStepNumber(); |
|||
using (var hashAlgorithm = new HMACSHA1(securityToken)) |
|||
{ |
|||
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); |
|||
} |
|||
} |
|||
|
|||
public bool ValidateCode(byte[] securityToken, int code, string modifier = null) |
|||
{ |
|||
if (securityToken == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(securityToken)); |
|||
} |
|||
|
|||
// Allow a variance of no greater than 9 minutes in either direction
|
|||
var currentTimeStep = GetCurrentTimeStepNumber(); |
|||
using (var hashAlgorithm = new HMACSHA1(securityToken)) |
|||
{ |
|||
for (var i = -2; i <= 2; i++) |
|||
{ |
|||
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); |
|||
if (computedTotp == code) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// No match
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
namespace LINGYUN.Abp.Identity.Security |
|||
{ |
|||
/// <summary>
|
|||
/// totp算法服务
|
|||
/// </summary>
|
|||
public interface ITotpService |
|||
{ |
|||
int GenerateCode(byte[] securityToken, string modifier = null); |
|||
|
|||
bool ValidateCode(byte[] securityToken, int code, string modifier = null); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
namespace LINGYUN.Abp.Identity |
|||
{ |
|||
/// <summary>
|
|||
/// 短信安全令牌验证缓存
|
|||
/// </summary>
|
|||
public class SmsSecurityTokenCacheItem |
|||
{ |
|||
/// <summary>
|
|||
/// 用于验证的Token
|
|||
/// </summary>
|
|||
public string Token { get; set; } |
|||
/// <summary>
|
|||
/// 用于验证的安全令牌
|
|||
/// </summary>
|
|||
public string SecurityToken { get; set; } |
|||
|
|||
public SmsSecurityTokenCacheItem() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public SmsSecurityTokenCacheItem(string token, string securityToken) |
|||
{ |
|||
Token = token; |
|||
SecurityToken = securityToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 生成查询Key
|
|||
/// </summary>
|
|||
/// <param name="phoneNumber">手机号</param>
|
|||
/// <param name="purpose">安全令牌用途</param>
|
|||
/// <returns></returns>
|
|||
public static string CalculateCacheKey(string phoneNumber, string purpose) |
|||
{ |
|||
return "Totp:" + purpose + ";p:" + phoneNumber; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.Localization; |
|||
using IIdentityUserRepository = LINGYUN.Abp.Identity.IIdentityUserRepository; |
|||
|
|||
namespace Microsoft.AspNetCore.Identity |
|||
{ |
|||
[Dependency(ServiceLifetime.Scoped, ReplaceServices = true)] |
|||
[ExposeServices(typeof(IUserValidator<IdentityUser>))] |
|||
public class PhoneNumberUserValidator : UserValidator<IdentityUser> |
|||
{ |
|||
private readonly IStringLocalizer _stringLocalizer; |
|||
private readonly IIdentityUserRepository _userRepository; |
|||
|
|||
public PhoneNumberUserValidator( |
|||
IIdentityUserRepository userRepository, |
|||
IStringLocalizer<IdentityResource> stringLocalizer) |
|||
{ |
|||
_userRepository = userRepository; |
|||
_stringLocalizer = stringLocalizer; |
|||
} |
|||
public override async Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user) |
|||
{ |
|||
var errors = new List<IdentityError>(); |
|||
await ValidatePhoneNumberAsync(manager, user, errors); |
|||
|
|||
return (errors.Count > 0) |
|||
? IdentityResult.Failed(errors.ToArray()) |
|||
: await base.ValidateAsync(manager, user); |
|||
} |
|||
|
|||
protected virtual async Task ValidatePhoneNumberAsync(UserManager<IdentityUser> manager, IdentityUser user, ICollection<IdentityError> errors) |
|||
{ |
|||
var phoneNumber = await manager.GetPhoneNumberAsync(user); |
|||
if (phoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
return; |
|||
} |
|||
var findUser = await _userRepository.FindByPhoneNumberAsync(phoneNumber, false); |
|||
if (findUser != null && !findUser.Id.Equals(user.Id)) |
|||
{ |
|||
//errors.Add(new IdentityError
|
|||
//{
|
|||
// Code = "DuplicatePhoneNumber",
|
|||
// Description = _stringLocalizer["DuplicatePhoneNumber", phoneNumber]
|
|||
//});
|
|||
throw new UserFriendlyException(_stringLocalizer["Volo.Abp.Identity:DuplicatePhoneNumber", phoneNumber]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.EntityFrameworkCore; |
|||
|
|||
namespace AuthServer.Host.EntityFrameworkCore.Identity |
|||
{ |
|||
public class EfCoreIdentityUserRepository : EfCoreRepository<IdentityDbContext, IdentityUser, Guid>, LINGYUN.Abp.Account.IIdentityUserRepository, |
|||
ITransientDependency |
|||
{ |
|||
public EfCoreIdentityUserRepository( |
|||
IDbContextProvider<IdentityDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public virtual async Task<bool> PhoneNumberHasRegistedAsync(string phoneNumber) |
|||
{ |
|||
return await DbSet.AnyAsync(x => x.PhoneNumberConfirmed && x.PhoneNumber.Equals(phoneNumber)); |
|||
} |
|||
|
|||
public virtual async Task<Guid?> GetIdByPhoneNumberAsync(string phoneNumber) |
|||
{ |
|||
return await DbSet |
|||
.Where(x => x.PhoneNumber.Equals(phoneNumber)) |
|||
.Select(x => x.Id) |
|||
.FirstOrDefaultAsync(); |
|||
} |
|||
|
|||
public virtual async Task<IdentityUser> FindByPhoneNumberAsync(string phoneNumber) |
|||
{ |
|||
return await WithDetails() |
|||
.Where(usr => usr.PhoneNumber.Equals(phoneNumber)) |
|||
.AsNoTracking() |
|||
.FirstOrDefaultAsync(); |
|||
} |
|||
|
|||
public override IQueryable<IdentityUser> WithDetails() |
|||
{ |
|||
return DbSet |
|||
.Include(x => x.Claims) |
|||
.Include(x => x.Roles) |
|||
.Include(x => x.Logins) |
|||
.Include(x => x.Tokens); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
<div style="position: absolute;"> |
|||
<span>{{L "EmailConfirmed" model.user}}</span> |
|||
<p style="display:block; padding:0 50px; width: 150px; height:48px; line-height:48px; color:#cc0000; font-size:26px; background:#9c9797; font-weight:bold;">{{model.code}}</p> |
|||
<span>{{L "EmailConfirmedRemarks"}}</span> |
|||
</div> |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Emailing.Templates; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.TextTemplating; |
|||
using Volo.Abp.Identity.Localization; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer4.Emailing.Templates |
|||
{ |
|||
public class IdentityEmailTemplateDefinitionProvider : TemplateDefinitionProvider |
|||
{ |
|||
public override void Define(ITemplateDefinitionContext context) |
|||
{ |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
IdentityEmailTemplates.EmailConfirmed, |
|||
displayName: LocalizableString.Create<IdentityResource>($"TextTemplate:{IdentityEmailTemplates.EmailConfirmed}"), |
|||
layout: StandardEmailTemplates.Layout, |
|||
localizationResource: typeof(IdentityResource) |
|||
).WithVirtualFilePath("/LINGYUN/Abp/IdentityServer4/Emailing/Templates/EmailConfirmed.tpl", true) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace LINGYUN.Abp.IdentityServer4.Emailing.Templates |
|||
{ |
|||
public static class IdentityEmailTemplates |
|||
{ |
|||
public const string EmailConfirmed = "Abp.Identity.EmailConfirmed"; |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Identity.EntityFrameworkCore; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer4.Identity |
|||
{ |
|||
public class EfCoreIdentityUserRepository : EfCoreRepository<IdentityDbContext, IdentityUser, Guid>, Abp.Account.IIdentityUserRepository, |
|||
ITransientDependency |
|||
{ |
|||
public EfCoreIdentityUserRepository( |
|||
IDbContextProvider<IdentityDbContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public virtual async Task<bool> PhoneNumberHasRegistedAsync(string phoneNumber) |
|||
{ |
|||
return await DbSet.AnyAsync(x => x.PhoneNumberConfirmed && x.PhoneNumber.Equals(phoneNumber)); |
|||
} |
|||
|
|||
public virtual async Task<Guid?> GetIdByPhoneNumberAsync(string phoneNumber) |
|||
{ |
|||
return await DbSet |
|||
.Where(x => x.PhoneNumber.Equals(phoneNumber)) |
|||
.Select(x => x.Id) |
|||
.FirstOrDefaultAsync(); |
|||
} |
|||
|
|||
public virtual async Task<IdentityUser> FindByPhoneNumberAsync(string phoneNumber) |
|||
{ |
|||
return await DbSet.Where(usr => usr.PhoneNumber.Equals(phoneNumber)) |
|||
.AsNoTracking() |
|||
.FirstOrDefaultAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using LINGYUN.Abp.Identity; |
|||
using LINGYUN.Abp.IdentityServer4.Emailing.Templates; |
|||
using Microsoft.Extensions.Localization; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Emailing; |
|||
using Volo.Abp.Identity.Localization; |
|||
using Volo.Abp.Sms; |
|||
using Volo.Abp.TextTemplating; |
|||
|
|||
namespace LINGYUN.Abp.IdentityServer4 |
|||
{ |
|||
public class UserSecurityCodeSender : IUserSecurityCodeSender, ITransientDependency |
|||
{ |
|||
protected IEmailSender EmailSender { get; } |
|||
protected ITemplateRenderer TemplateRenderer { get; } |
|||
protected IStringLocalizer<IdentityResource> Localizer { get; } |
|||
|
|||
protected ISmsSender SmsSender { get; } |
|||
|
|||
public UserSecurityCodeSender( |
|||
ISmsSender smsSender, |
|||
IEmailSender emailSender, |
|||
ITemplateRenderer templateRenderer, |
|||
IStringLocalizer<IdentityResource> localizer) |
|||
{ |
|||
SmsSender = smsSender; |
|||
EmailSender = emailSender; |
|||
TemplateRenderer = templateRenderer; |
|||
Localizer = localizer; |
|||
} |
|||
|
|||
public virtual async Task SendEmailConfirmedCodeAsync( |
|||
string userName, |
|||
string email, |
|||
string token, |
|||
CancellationToken cancellation = default) |
|||
{ |
|||
var emailContent = await TemplateRenderer.RenderAsync( |
|||
IdentityEmailTemplates.EmailConfirmed, |
|||
new { user = userName, code = token }); |
|||
|
|||
await EmailSender.SendAsync( |
|||
email, |
|||
Localizer["EmailConfirmed"], |
|||
emailContent); |
|||
} |
|||
|
|||
public virtual async Task SendPhoneConfirmedCodeAsync( |
|||
string phone, |
|||
string token, |
|||
string template, |
|||
CancellationToken cancellation = default) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(template, nameof(template)); |
|||
|
|||
var smsMessage = new SmsMessage(phone, token); |
|||
smsMessage.Properties.Add("code", token); |
|||
smsMessage.Properties.Add("TemplateCode", template); |
|||
|
|||
await SmsSender.SendAsync(smsMessage); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue