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 |
namespace LINGYUN.Abp.Account |
||||
{ |
{ |
||||
[DependsOn(typeof(AbpAccountDomainSharedModule))] |
[DependsOn( |
||||
|
typeof(Volo.Abp.Account.AbpAccountApplicationContractsModule))] |
||||
public class AbpAccountApplicationContractsModule : AbpModule |
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.Auditing; |
||||
using Volo.Abp.Identity; |
using Volo.Abp.Identity; |
||||
using Volo.Abp.Validation; |
using Volo.Abp.Validation; |
||||
|
|
||||
namespace LINGYUN.Abp.Account |
namespace LINGYUN.Abp.Account |
||||
{ |
{ |
||||
public class PhoneNumberRegisterDto |
public class PhoneRegisterDto |
||||
{ |
{ |
||||
[Required] |
[Required] |
||||
[Phone] |
[Phone] |
||||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] |
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))] |
||||
|
[Display(Name = "PhoneNumber")] |
||||
public string PhoneNumber { get; set; } |
public string PhoneNumber { get; set; } |
||||
|
|
||||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))] |
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))] |
||||
|
[DisplayName("Name")] |
||||
public string Name { get; set; } |
public string Name { get; set; } |
||||
|
|
||||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] |
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] |
||||
|
[DisplayName("UserName")] |
||||
public string UserName { get; set; } |
public string UserName { get; set; } |
||||
|
|
||||
[EmailAddress] |
[EmailAddress] |
||||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] |
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))] |
||||
|
[DisplayName("EmailAddress")] |
||||
public string EmailAddress { get; set; } |
public string EmailAddress { get; set; } |
||||
|
|
||||
[Required] |
[Required] |
||||
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))] |
||||
[DataType(DataType.Password)] |
[DataType(DataType.Password)] |
||||
|
[DisplayName("Password")] |
||||
[DisableAuditing] |
[DisableAuditing] |
||||
public string Password { get; set; } |
public string Password { get; set; } |
||||
|
|
||||
[Required] |
[Required] |
||||
[StringLength(6)] |
[StringLength(6,MinimumLength = 6)] |
||||
public string VerifyCode { get; set; } |
[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 System.Threading.Tasks; |
||||
using Volo.Abp.Application.Services; |
using Volo.Abp.Application.Services; |
||||
using Volo.Abp.Identity; |
|
||||
|
|
||||
namespace LINGYUN.Abp.Account |
namespace LINGYUN.Abp.Account |
||||
{ |
{ |
||||
public interface IAccountAppService : IApplicationService |
public interface IAccountAppService : IApplicationService |
||||
{ |
{ |
||||
Task<IdentityUserDto> RegisterAsync(PhoneNumberRegisterDto input); |
/// <summary>
|
||||
|
/// 通过手机号注册用户账户
|
||||
Task<IdentityUserDto> RegisterAsync(WeChatRegisterDto input); |
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
Task ResetPasswordAsync(PasswordResetDto input); |
/// <returns></returns>
|
||||
|
Task RegisterAsync(PhoneRegisterDto input); |
||||
Task VerifyPhoneNumberAsync(VerifyDto 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 |
namespace LINGYUN.Abp.Identity |
||||
{ |
{ |
||||
public class IdentityUserTwoFactorEnabledDto |
public class ChangeTwoFactorEnabledDto |
||||
{ |
{ |
||||
public bool Enabled { get; set; } |
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