diff --git a/IRaCIS.Core.API/Controllers/ExtraController.cs b/IRaCIS.Core.API/Controllers/ExtraController.cs index 7a3b3504d..eef01245d 100644 --- a/IRaCIS.Core.API/Controllers/ExtraController.cs +++ b/IRaCIS.Core.API/Controllers/ExtraController.cs @@ -30,6 +30,8 @@ using IRaCIS.Core.Application.Helper; using Microsoft.Extensions.Options; using IRaCIS.Core.Application.Contracts; using LoginReturnDTO = IRaCIS.Application.Contracts.LoginReturnDTO; +using DocumentFormat.OpenXml.Spreadsheet; +using AutoMapper.QueryableExtensions; namespace IRaCIS.Api.Controllers { @@ -53,8 +55,8 @@ namespace IRaCIS.Api.Controllers /// /// [HttpGet, Route("doctor/getDetail/{doctorId:guid}")] - - public async Task> GetDoctorDetail([FromServices] IAttachmentService attachmentService, [FromServices] IDoctorService _doctorService, + + public async Task> GetDoctorDetail([FromServices] IAttachmentService attachmentService, [FromServices] IDoctorService _doctorService, [FromServices] IEducationService _educationService, [FromServices] ITrialExperienceService _trialExperienceService, [FromServices] IResearchPublicationService _researchPublicationService, [FromServices] IVacationService _vacationService, Guid doctorId) @@ -66,7 +68,7 @@ namespace IRaCIS.Api.Controllers var doctorDetail = new DoctorDetailDTO { - AuditView =await _doctorService.GetAuditState(doctorId), + AuditView = await _doctorService.GetAuditState(doctorId), BasicInfoView = await _doctorService.GetBasicInfo(doctorId), EmploymentView = await _doctorService.GetEmploymentInfo(doctorId), AttachmentList = await attachmentService.GetAttachments(doctorId), @@ -77,7 +79,7 @@ namespace IRaCIS.Api.Controllers TrialExperienceView = await _trialExperienceService.GetTrialExperience(doctorId), ResearchPublicationView = await _researchPublicationService.GetResearchPublication(doctorId), - SpecialtyView =await _doctorService.GetSpecialtyInfo(doctorId), + SpecialtyView = await _doctorService.GetSpecialtyInfo(doctorId), InHoliday = (await _vacationService.OnVacation(doctorId)).IsSuccess, IntoGroupInfo = _doctorService.GetDoctorIntoGroupInfo(doctorId), SowList = sowList, @@ -96,80 +98,30 @@ namespace IRaCIS.Api.Controllers /// 系统用户登录接口[New] [HttpPost, Route("user/login")] [AllowAnonymous] - public async Task> Login(UserLoginDTO loginUser, [FromServices] IEasyCachingProvider provider, [FromServices] IUserService _userService, - [FromServices] ITokenService _tokenService, - - [FromServices] IReadingImageTaskService readingImageTaskService, - [FromServices] IConfiguration configuration) + public async Task Login(UserLoginDTO loginUser, + [FromServices] IEasyCachingProvider provider, + [FromServices] IUserService _userService, + [FromServices] ITokenService _tokenService, + [FromServices] IReadingImageTaskService readingImageTaskService, + IOptionsMonitor _verifyConfig, + IMailVerificationService _mailVerificationService) { - - var returnModel = await _userService.Login(loginUser.UserName, loginUser.Password); - - if (returnModel.IsSuccess) + //MFA 邮箱验证 前端传递用户Id 和MFACode + if (loginUser.UserId != null && !string.IsNullOrEmpty(loginUser.MFACode) && _verifyConfig.CurrentValue.OpenLoginMFA) { - #region GRPC 调用鉴权中心,因为服务器IIS问题 http/2 故而没法使用 + Guid userId = (Guid)loginUser.UserId; - ////重试策略 - //var defaultMethodConfig = new MethodConfig - //{ - // Names = { MethodName.Default }, - // RetryPolicy = new RetryPolicy - // { - // MaxAttempts = 3, - // InitialBackoff = TimeSpan.FromSeconds(1), - // MaxBackoff = TimeSpan.FromSeconds(5), - // BackoffMultiplier = 1.5, - // RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable } - // } - //}; + //验证MFA 编码是否有问题 - //#region unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate + await _userService.VerifyMFACodeAsync(userId, loginUser.MFACode); - //var httpHandler = new HttpClientHandler(); - //// Return `true` to allow certificates that are untrusted/invalid - //httpHandler.ServerCertificateCustomValidationCallback = - // HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + var basicInfo = await _userService.GetUserBasicInfo(userId); + var loginReturn = new LoginReturnDTO() { BasicInfo = basicInfo }; - //////这一句是让grpc支持本地 http 如果本地访问部署在服务器上,那么是访问不成功的 - //AppContext.SetSwitch( - // "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + loginReturn.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(loginReturn.BasicInfo)); - //#endregion - - - - //var grpcAdress = configuration.GetValue("GrpcAddress"); - ////var grpcAdress = "http://localhost:7200"; - - //var channel = GrpcChannel.ForAddress(grpcAdress, new GrpcChannelOptions - //{ - // HttpHandler = httpHandler, - // ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } - - //}); - ////var channel = GrpcChannel.ForAddress(grpcAdress); - //var grpcClient = new TokenGrpcService.TokenGrpcServiceClient(channel); - - //var userInfo = returnModel.Data.BasicInfo; - - //var tokenResponse = grpcClient.GetUserToken(new GetTokenReuqest() - //{ - // Id = userInfo.Id.ToString(), - // ReviewerCode = userInfo.ReviewerCode, - // IsAdmin = userInfo.IsAdmin, - // RealName = userInfo.RealName, - // UserTypeEnumInt = (int)userInfo.UserTypeEnum, - // UserTypeShortName = userInfo.UserTypeShortName, - // UserName = userInfo.UserName - //}); - - //returnModel.Data.JWTStr = tokenResponse.Token; - - #endregion - - returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo)); // 创建一个 CookieOptions 对象,用于设置 Cookie 的属性 var option = new CookieOptions @@ -180,20 +132,161 @@ namespace IRaCIS.Api.Controllers Secure = false // 确保 cookie 只能通过 HTTPS 访问 }; - HttpContext.Response.Cookies.Append("access_token", returnModel.Data.JWTStr, option); + HttpContext.Response.Cookies.Append("access_token", loginReturn.JWTStr, option); + + // 验证阅片休息时间 + await readingImageTaskService.ResetReadingRestTime(userId); + + await provider.SetAsync(userId.ToString(), loginReturn.JWTStr, TimeSpan.FromDays(7)); + + await provider.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes)); + + return ResponseOutput.Ok(loginReturn); + + } + else + { + var returnModel = await _userService.Login(loginUser.UserName, loginUser.Password); + + if (returnModel.IsSuccess) + { + #region GRPC 调用鉴权中心,因为服务器IIS问题 http/2 故而没法使用 + + ////重试策略 + //var defaultMethodConfig = new MethodConfig + //{ + // Names = { MethodName.Default }, + // RetryPolicy = new RetryPolicy + // { + // MaxAttempts = 3, + // InitialBackoff = TimeSpan.FromSeconds(1), + // MaxBackoff = TimeSpan.FromSeconds(5), + // BackoffMultiplier = 1.5, + // RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable } + // } + //}; + + //#region unable to trust the certificate then the gRPC client can be configured to ignore the invalid certificate + + //var httpHandler = new HttpClientHandler(); + //// Return `true` to allow certificates that are untrusted/invalid + //httpHandler.ServerCertificateCustomValidationCallback = + // HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + + + //////这一句是让grpc支持本地 http 如果本地访问部署在服务器上,那么是访问不成功的 + //AppContext.SetSwitch( + // "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + //#endregion + + + + //var grpcAdress = configuration.GetValue("GrpcAddress"); + ////var grpcAdress = "http://localhost:7200"; + + //var channel = GrpcChannel.ForAddress(grpcAdress, new GrpcChannelOptions + //{ + // HttpHandler = httpHandler, + // ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } } + + //}); + ////var channel = GrpcChannel.ForAddress(grpcAdress); + //var grpcClient = new TokenGrpcService.TokenGrpcServiceClient(channel); + + //var userInfo = returnModel.Data.BasicInfo; + + //var tokenResponse = grpcClient.GetUserToken(new GetTokenReuqest() + //{ + // Id = userInfo.Id.ToString(), + // ReviewerCode = userInfo.ReviewerCode, + // IsAdmin = userInfo.IsAdmin, + // RealName = userInfo.RealName, + // UserTypeEnumInt = (int)userInfo.UserTypeEnum, + // UserTypeShortName = userInfo.UserTypeShortName, + // UserName = userInfo.UserName + //}); + + //returnModel.Data.JWTStr = tokenResponse.Token; + + #endregion + + var userId = returnModel.Data.BasicInfo.Id; + + if (_verifyConfig.CurrentValue.OpenLoginMFA) + { + //发版屏蔽 + + returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo)); + + //MFA 发送邮件 + + returnModel.Data.IsMFA = true; + + var email = returnModel.Data.BasicInfo.EMail; + + #region 隐藏Email + // 找到 "@" 符号的位置 + int atIndex = email.IndexOf('@'); + + // 替换 "@" 符号前的中间两位为星号 + string visiblePart = email.Substring(0, atIndex); + + int startIndex = (visiblePart.Length - 2) / 2; + + // 替换中间两位字符为星号 + string hiddenPartBeforeAt = visiblePart.Substring(0, startIndex) + "**" + visiblePart.Substring(startIndex + 2); + + string afterAt = email.Substring(atIndex + 1); + + // 组合隐藏和可见部分 + string hiddenEmail = hiddenPartBeforeAt + "@" + afterAt; + #endregion + + returnModel.Data.BasicInfo.EMail = hiddenEmail; + + await _userService.SendMFAEmail(userId); + + } + else + { + returnModel.Data.JWTStr = _tokenService.GetToken(IRaCISClaims.Create(returnModel.Data.BasicInfo)); + + // 创建一个 CookieOptions 对象,用于设置 Cookie 的属性 + var option = new CookieOptions + { + Expires = DateTime.Now.AddMonths(1), // 设置过期时间为 30 分钟之后 + HttpOnly = false, // 确保 cookie 只能通过 HTTP 访问 + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None, // 设置 SameSite 属性 + Secure = false // 确保 cookie 只能通过 HTTPS 访问 + }; + + HttpContext.Response.Cookies.Append("access_token", returnModel.Data.JWTStr, option); + + + + // 验证阅片休息时间 + await readingImageTaskService.ResetReadingRestTime(returnModel.Data.BasicInfo.Id); + + await provider.SetAsync(userId.ToString(), returnModel.Data.JWTStr, TimeSpan.FromDays(7)); + + await provider.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes)); + } + + } + return returnModel; } - var userId = returnModel.Data.BasicInfo.Id.ToString(); - //provider.Set(userId, userId, TimeSpan.FromMinutes(AppSettings.LoginExpiredTimeSpan)); - // 验证阅片休息时间 - await readingImageTaskService.ResetReadingRestTime(returnModel.Data.BasicInfo.Id); - await provider.SetAsync(userId.ToString(), returnModel.Data.JWTStr, TimeSpan.FromDays(7)); - return returnModel; + + + } + + [HttpGet, Route("imageShare/ShareImage")] [AllowAnonymous] public IResponseOutput ShareImage([FromServices] ITokenService _tokenService) @@ -223,7 +316,7 @@ namespace IRaCIS.Api.Controllers var ossOptions = serviceOption.AliyunOSS; - return ResponseOutput.Ok(new ObjectStoreDTO() { ObjectStoreUse = serviceOption.ObjectStoreUse, MinIO = serviceOption.MinIO ,AliyunOSS= serviceOption.AliyunOSS,AWS=serviceOption.AWS }); + return ResponseOutput.Ok(new ObjectStoreDTO() { ObjectStoreUse = serviceOption.ObjectStoreUse, MinIO = serviceOption.MinIO, AliyunOSS = serviceOption.AliyunOSS, AWS = serviceOption.AWS }); #region 临时token 屏蔽 //IClientProfile profile = DefaultProfile.GetProfile(ossOptions.RegionId, ossOptions.AccessKeyId, ossOptions.AccessKeySecret); @@ -264,19 +357,19 @@ namespace IRaCIS.Api.Controllers #endregion } - else if(Enum.TryParse(serviceOption.ObjectStoreUse, out var parsedValue) && parsedValue == ObjectStoreUse.MinIO) + else if (Enum.TryParse(serviceOption.ObjectStoreUse, out var parsedValue) && parsedValue == ObjectStoreUse.MinIO) { - return ResponseOutput.Ok(new ObjectStoreDTO() { ObjectStoreUse=serviceOption.ObjectStoreUse,MinIO=serviceOption.MinIO, AWS = serviceOption.AWS }); + return ResponseOutput.Ok(new ObjectStoreDTO() { ObjectStoreUse = serviceOption.ObjectStoreUse, MinIO = serviceOption.MinIO, AWS = serviceOption.AWS }); } else { return ResponseOutput.Ok(new ObjectStoreDTO() { ObjectStoreUse = serviceOption.ObjectStoreUse, MinIO = serviceOption.MinIO, AWS = serviceOption.AWS }); } - + } - [HttpGet("user/GenerateSTS")] - public IResponseOutput GenerateSTS([FromServices]IOptionsMonitor options ) + [HttpGet("user/GenerateSTS")] + public IResponseOutput GenerateSTS([FromServices] IOptionsMonitor options) { var ossOptions = options.CurrentValue; @@ -304,9 +397,9 @@ namespace IRaCIS.Api.Controllers SecurityToken = response.Credentials.SecurityToken, Expiration = response.Credentials.Expiration, - Region = ossOptions.region , - BucketName = ossOptions.bucketName , - ViewEndpoint = ossOptions.viewEndpoint , + Region = ossOptions.region, + BucketName = ossOptions.bucketName, + ViewEndpoint = ossOptions.viewEndpoint, }; @@ -318,12 +411,12 @@ namespace IRaCIS.Api.Controllers [HttpGet("User/UserRedirect")] [AllowAnonymous] - public async Task UserRedirect([FromServices] IRepository _userRepository, string url ,[FromServices]ILogger _logger) + public async Task UserRedirect([FromServices] IRepository _userRepository, string url, [FromServices] ILogger _logger) { var decodeUrl = System.Web.HttpUtility.UrlDecode(url); - var userId = decodeUrl.Substring(decodeUrl.IndexOf("UserId=") + "UserId=".Length , 36) ; + var userId = decodeUrl.Substring(decodeUrl.IndexOf("UserId=") + "UserId=".Length, 36); var token = decodeUrl.Substring(decodeUrl.IndexOf("access_token=") + "access_token=".Length); @@ -331,12 +424,12 @@ namespace IRaCIS.Api.Controllers var domainStrList = decodeUrl.Split("/").ToList().Take(3).ToList(); - var errorUrl = domainStrList[0]+"//"+ domainStrList[2]+ "/error"; + var errorUrl = domainStrList[0] + "//" + domainStrList[2] + "/error"; - if (!await _userRepository.AnyAsync(t => t.Id == Guid.Parse(userId) && t.EmailToken == token && t.IsFirstAdd)) + if (!await _userRepository.AnyAsync(t => t.Id == Guid.Parse(userId) && t.EmailToken == token && t.IsFirstAdd)) { - decodeUrl = errorUrl+ $"?lang={lang}&ErrorMessage={System.Web.HttpUtility.UrlEncode(lang=="zh"? "您的初始化链接已过期": "Error!The initialization link has expired. Return")} "; + decodeUrl = errorUrl + $"?lang={lang}&ErrorMessage={System.Web.HttpUtility.UrlEncode(lang == "zh" ? "您的初始化链接已过期" : "Error!The initialization link has expired. Return")} "; } return Redirect(decodeUrl); diff --git a/IRaCIS.Core.API/IRaCIS.Core.API.xml b/IRaCIS.Core.API/IRaCIS.Core.API.xml index 3580ca96a..ddbe96072 100644 --- a/IRaCIS.Core.API/IRaCIS.Core.API.xml +++ b/IRaCIS.Core.API/IRaCIS.Core.API.xml @@ -29,7 +29,7 @@ - + 系统用户登录接口[New] diff --git a/IRaCIS.Core.API/appsettings.Test_IRC.json b/IRaCIS.Core.API/appsettings.Test_IRC.json index 9e91ec0c2..6baf65bb9 100644 --- a/IRaCIS.Core.API/appsettings.Test_IRC.json +++ b/IRaCIS.Core.API/appsettings.Test_IRC.json @@ -60,7 +60,10 @@ "LoginMaxFailCount": 5, "LoginFailLockMinutes": 1, - "AutoLoginOutMinutes": 1 + + "AutoLoginOutMinutes": 1, + + "OpenLoginMFA": true }, "SystemEmailSendConfig": { diff --git a/IRaCIS.Core.Application/Service/Common/MailService.cs b/IRaCIS.Core.Application/Service/Common/MailService.cs index 4ebc148eb..3acfa9ebd 100644 --- a/IRaCIS.Core.Application/Service/Common/MailService.cs +++ b/IRaCIS.Core.Application/Service/Common/MailService.cs @@ -21,6 +21,8 @@ namespace IRaCIS.Application.Services Task SiteSurveyRejectEmail(MimeMessage messageToSend); + Task SenMFAVerifyEmail(Guid userId, string userName, string emailAddress, int verificationCode); + Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode); Task AnolymousSendEmailForResetAccount(string emailAddress, int verificationCode); @@ -91,6 +93,66 @@ namespace IRaCIS.Application.Services return str; } + //MFA + public async Task SenMFAVerifyEmail(Guid userId, string userName, string emailAddress, int verificationCode) + { + var messageToSend = new MimeMessage(); + //发件地址 + messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail)); + //收件地址 + messageToSend.To.Add(new MailboxAddress(userName, emailAddress)); + //主题 + //---[来自{0}] 关于MFA邮箱验证的提醒 + messageToSend.Subject = _localizer["Mail_EmailMFATopic", _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN]; + + var builder = new BodyBuilder(); + + + var pathToFile = _hostEnvironment.WebRootPath + + Path.DirectorySeparatorChar.ToString() + + "EmailTemplate" + + Path.DirectorySeparatorChar.ToString() + //+ "UserOptCommon.html"; + + (_userInfo.IsEn_Us ? "UserOptCommon_US.html" : "UserOptCommon.html"); + + using (StreamReader SourceReader = System.IO.File.OpenText(pathToFile)) + { + var templateInfo = SourceReader.ReadToEnd(); + + + builder.HtmlBody = string.Format(ReplaceCompanyName(templateInfo), + + userName, + _localizer["Mail_MFAEmail"], + verificationCode + ); + } + + + messageToSend.Body = builder.ToMessageBody(); + + + + EventHandler sucessHandle = (sender, args) => + { + // args.Response + var code = verificationCode.ToString(); + _ = _verificationCodeRepository.AddAsync(new VerificationCode() + { + CodeType = 0, + HasSend = true, + Code = code, + UserId = userId, + ExpirationTime = DateTime.Now.AddMinutes(3) + }).Result; + _ = _verificationCodeRepository.SaveChangesAsync().Result; + + }; + + + await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig, sucessHandle); + } + //重置邮箱 public async Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode) { diff --git a/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs b/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs index 24fb5e16f..83adc07d9 100644 --- a/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs +++ b/IRaCIS.Core.Application/Service/Management/DTO/UserModel.cs @@ -21,6 +21,9 @@ namespace IRaCIS.Application.Contracts { public string UserName { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; + + public Guid? UserId { get; set; } + public string MFACode { get; set; } = string.Empty; } public class LoginReturnDTO @@ -28,6 +31,8 @@ namespace IRaCIS.Application.Contracts public UserBasicInfo BasicInfo { get; set; } = new UserBasicInfo(); public string JWTStr { get; set; }=string.Empty; + public bool IsMFA { get; set; } = false; + } public class UserBasicInfo @@ -59,7 +64,7 @@ namespace IRaCIS.Application.Contracts public string PermissionStr { get; set; } = String.Empty; - + public string EMail { get; set; } = string.Empty; public bool IsFirstAdd { get; set; } public bool IsReviewer { get; set; } = false; diff --git a/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs b/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs index cc44a25ed..ddf76bd63 100644 --- a/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs +++ b/IRaCIS.Core.Application/Service/Management/Interface/IUserService.cs @@ -10,12 +10,16 @@ namespace IRaCIS.Application.Services Task GetUser(Guid id); Task> GetUserList(UserListQueryDTO param); Task> Login(string userName, string password); + Task VerifyMFACodeAsync(Guid userId, string Code); + + Task SendMFAEmail(Guid userId); + Task GetUserBasicInfo(Guid userId); Task ModifyPassword(EditPasswordCommand editPwModel); Task ResetPassword(Guid userId); Task UpdateUser(UserCommand model); Task UpdateUserState(Guid userId, UserStateEnum state); - + //Task SendVerificationCode(string emailOrPhone, VerifyType verificationType, bool isReviewer = false); //Task SetNewPassword(ResetPasswordCommand resetPwdModel); } diff --git a/IRaCIS.Core.Application/Service/Management/UserService.cs b/IRaCIS.Core.Application/Service/Management/UserService.cs index 6490c9bfb..972ffcc93 100644 --- a/IRaCIS.Core.Application/Service/Management/UserService.cs +++ b/IRaCIS.Core.Application/Service/Management/UserService.cs @@ -15,6 +15,9 @@ using Medallion.Threading; using EasyCaching.Core; using IRaCIS.Core.Application.Contracts; using LoginReturnDTO = IRaCIS.Application.Contracts.LoginReturnDTO; +using IRaCIS.Core.Application.Auth; +using BeetleX.Redis.Commands; +using IRaCIS.Core.Domain.Models; namespace IRaCIS.Application.Services { @@ -22,17 +25,17 @@ namespace IRaCIS.Application.Services public class UserService : BaseService, IUserService { private readonly IRepository _userRepository; - - private readonly IMailVerificationService _mailVerificationService; + + private readonly IMailVerificationService _mailVerificationService; private readonly IRepository _verificationCodeRepository; private readonly IRepository _doctorRepository; private readonly IRepository _userTrialRepository; private readonly IRepository _userLogRepository; - private readonly IRepository _userPassWordLogRepository; - private readonly IDistributedLockProvider _distributedLockProvider; + private readonly IRepository _userPassWordLogRepository; + private readonly IDistributedLockProvider _distributedLockProvider; private readonly IEasyCachingProvider _cache; - private readonly IReadingImageTaskService _readingImageTaskService; - private readonly IOptionsMonitor _verifyConfig; + private readonly IReadingImageTaskService _readingImageTaskService; + private readonly IOptionsMonitor _verifyConfig; public UserService(IRepository userRepository, @@ -41,20 +44,20 @@ namespace IRaCIS.Application.Services IRepository verificationCodeRepository, IRepository doctorRepository, IEasyCachingProvider cache, - IReadingImageTaskService readingImageTaskService, - IRepository userTrialRepository, + IReadingImageTaskService readingImageTaskService, + IRepository userTrialRepository, IOptionsMonitor verifyConfig, IRepository userLogRepository, - IRepository userPassWordLogRepository + IRepository userPassWordLogRepository , IDistributedLockProvider distributedLockProvider) { _userLogRepository = userLogRepository; - this._userPassWordLogRepository = userPassWordLogRepository; - _verifyConfig = verifyConfig; + this._userPassWordLogRepository = userPassWordLogRepository; + _verifyConfig = verifyConfig; _cache = cache; - this._readingImageTaskService = readingImageTaskService; - _userRepository = userRepository; + this._readingImageTaskService = readingImageTaskService; + _userRepository = userRepository; _mailVerificationService = mailVerificationService; _verificationCodeRepository = verificationCodeRepository; _doctorRepository = doctorRepository; @@ -95,45 +98,45 @@ namespace IRaCIS.Application.Services private async Task VerifyUserPwdAsync(Guid userId, string newPwd, string? oldPwd = null) { - //var dbUser = (await _userRepository.FirstOrDefaultAsync(t => t.Id == userId)).IfNullThrowException(); + //var dbUser = (await _userRepository.FirstOrDefaultAsync(t => t.Id == userId)).IfNullThrowException(); - if (oldPwd != null && oldPwd == newPwd) - { - //---新密码与旧密码相同。 - throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]); - } + if (oldPwd != null && oldPwd == newPwd) + { + //---新密码与旧密码相同。 + throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]); + } - var dbUser = (await _userRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException(); + var dbUser = (await _userRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException(); - if (oldPwd != null && dbUser.Password != oldPwd) - { - //---旧密码验证失败。 - throw new BusinessValidationFailedException(_localizer["User_OldPwdInvalid"]); - } + if (oldPwd != null && dbUser.Password != oldPwd) + { + //---旧密码验证失败。 + throw new BusinessValidationFailedException(_localizer["User_OldPwdInvalid"]); + } - if (dbUser.Password == newPwd) - { - //---新密码与旧密码相同。 - throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]); - } + if (dbUser.Password == newPwd) + { + //---新密码与旧密码相同。 + throw new BusinessValidationFailedException(_localizer["User_NewOldPwdSame"]); + } var passWordList = await _userPassWordLogRepository.Where(x => x.UserId == userId).OrderByDescending(x => x.CreateTime).Take(2).ToListAsync(); if (passWordList.Any(x => x.PassWord == newPwd)) { - throw new BusinessValidationFailedException(_localizer["User_PassWordRepeat"]); - } + throw new BusinessValidationFailedException(_localizer["User_PassWordRepeat"]); + } if (oldPwd != null) { - await _userPassWordLogRepository.AddAsync(new UserPassWordLog() - { + await _userPassWordLogRepository.AddAsync(new UserPassWordLog() + { + + CreateTime = DateTime.Now, + PassWord = oldPwd, + UserId = userId, + }); + } - CreateTime = DateTime.Now, - PassWord = oldPwd, - UserId = userId, - }); - } - await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == userId, x => new User() { @@ -142,7 +145,7 @@ namespace IRaCIS.Application.Services await _userPassWordLogRepository.SaveChangesAsync(); - await Task.CompletedTask; + await Task.CompletedTask; } @@ -306,7 +309,7 @@ namespace IRaCIS.Application.Services { await _mailVerificationService.AdminResetPwdSendEmailAsync(userId, pwd); } - catch (Exception ) + catch (Exception) { //---请检查邮箱地址或者联系维护人员, 邮件发送失败, 未能创建账户成功 throw new BusinessValidationFailedException(_localizer["User_CreateFailed"]); @@ -319,7 +322,7 @@ namespace IRaCIS.Application.Services IsFirstAdd = true }); - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId=userId, OptType = UserOptType.ResetPassword }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = UserOptType.ResetPassword }, true); return ResponseOutput.Ok(); } @@ -403,7 +406,7 @@ namespace IRaCIS.Application.Services } } - var list = await _userRepository.Where(t => t.EMail == email && t.Status== UserStateEnum.Enable).Select(t => new UserAccountDto() { UserId = t.Id, UserName = t.UserName, UserRealName = t.FullName, UserType = t.UserTypeRole.UserTypeShortName }).ToListAsync(); + var list = await _userRepository.Where(t => t.EMail == email && t.Status == UserStateEnum.Enable).Select(t => new UserAccountDto() { UserId = t.Id, UserName = t.UserName, UserRealName = t.FullName, UserType = t.UserTypeRole.UserTypeShortName }).ToListAsync(); @@ -431,7 +434,7 @@ namespace IRaCIS.Application.Services IsFirstAdd = false }); - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId,LoginPassword=newPwd, OptType = UserOptType.UnloginModifyPasswoed }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, LoginPassword = newPwd, OptType = UserOptType.UnloginModifyPasswoed }, true); return ResponseOutput.Result(success); @@ -467,7 +470,7 @@ namespace IRaCIS.Application.Services IsFirstAdd = false }); - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= _userInfo.Id, OptType = UserOptType.LoginModifyPassword }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = _userInfo.Id, OptType = UserOptType.LoginModifyPassword }, true); return ResponseOutput.Result(success); @@ -555,11 +558,11 @@ namespace IRaCIS.Application.Services var success = await _userRepository.SaveChangesAsync(); } - + await _mailVerificationService.AddUserSendEmailAsync(saveItem.Id, userAddModel.BaseUrl, userAddModel.RouteUrl); - return ResponseOutput.Ok( new UserAddedReturnDTO { Id = saveItem.Id, UserCode = saveItem.UserCode }); + return ResponseOutput.Ok(new UserAddedReturnDTO { Id = saveItem.Id, UserCode = saveItem.UserCode }); } @@ -589,7 +592,7 @@ namespace IRaCIS.Application.Services user.OrganizationName = AppSettings.DefaultInternalOrganizationName; } - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= model.Id , OptType = UserOptType.UpdateUser }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = model.Id, OptType = UserOptType.UpdateUser }, true); var success = await _userRepository.SaveChangesAsync(); @@ -611,7 +614,7 @@ namespace IRaCIS.Application.Services return ResponseOutput.NotOk(_localizer["User_InProject"]); } - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId= userId, OptType = UserOptType.DeleteUser }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = UserOptType.DeleteUser }, true); var success = await _userRepository.BatchDeleteNoTrackingAsync(t => t.Id == userId); @@ -629,7 +632,7 @@ namespace IRaCIS.Application.Services public async Task UpdateUserState(Guid userId, UserStateEnum state) { - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = state==UserStateEnum.Enable? UserOptType.AccountEnable: UserOptType.AccountLocked }, true); + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = _userInfo.Id, OptUserId = userId, OptType = state == UserStateEnum.Enable ? UserOptType.AccountEnable : UserOptType.AccountLocked }, true); var success = await _userRepository.BatchUpdateNoTrackingAsync(u => u.Id == userId, t => new User { @@ -639,7 +642,70 @@ namespace IRaCIS.Application.Services } + public async Task GetUserBasicInfo(Guid userId) + { + var info = await _userRepository.Where(u => u.Id == userId).ProjectTo(_mapper.ConfigurationProvider).FirstNotNullAsync(); + return info; + } + + /// + /// 发送MFA 验证邮件 + /// + /// + /// + [AllowAnonymous] + public async Task SendMFAEmail(Guid userId) + { + var userInfo = await _userRepository.Where(u => u.Id == userId).Select(t => new { t.FullName, t.EMail }).FirstOrDefaultAsync(); + + int verificationCode = new Random().Next(100000, 1000000); + + await _mailVerificationService.SenMFAVerifyEmail(userId, userInfo.FullName, userInfo.EMail, verificationCode); + + return ResponseOutput.Ok(); + } + + /// + /// 验证MFA 邮件 + /// + /// + /// + /// + /// + [AllowAnonymous] + public async Task VerifyMFACodeAsync(Guid userId, string Code) + { + var verificationRecord = await _repository.GetQueryable().OrderByDescending(x => x.ExpirationTime).Where(t => t.UserId == userId && t.Code == Code && t.CodeType == VerifyType.Email).FirstOrDefaultAsync(); + VerifyEmialGetDoctorInfoOutDto result = new VerifyEmialGetDoctorInfoOutDto(); + + //检查数据库是否存在该验证码 + if (verificationRecord == null) + { + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALoginFail }, true); + //---验证码错误。 + throw new BusinessValidationFailedException(_localizer["TrialSiteSurvey_WrongVerificationCode"]); + } + else + { + //检查验证码是否失效 + if (verificationRecord.ExpirationTime < DateTime.Now) + { + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALoginFail }, true); + //---验证码已经过期。 + throw new BusinessValidationFailedException(_localizer["TrialSiteSurvey_ExpiredVerificationCode"]); + + + } + else //验证码正确 并且 没有超时 + { + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = userId, OptUserId = userId, OptType = UserOptType.MFALogin }, true); + + } + } + + return ResponseOutput.Ok(); + } /// /// 用户登陆 @@ -690,14 +756,12 @@ namespace IRaCIS.Application.Services failCount++; _cache.Set(cacheKey, failCount, TimeSpan.FromMinutes(lockoutMinutes)); - var errorPwdUserId = await _userRepository.Where(u => u.UserName==userName).Select(t=>t.Id).FirstOrDefaultAsync(); + var errorPwdUserId = await _userRepository.Where(u => u.UserName == userName).Select(t => t.Id).FirstOrDefaultAsync(); await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = errorPwdUserId, OptUserId = errorPwdUserId, LoginFaildName = userName, LoginPassword = password, OptType = UserOptType.AccountOrPasswordError }, true); return ResponseOutput.NotOk(_localizer["User_CheckNameOrPw"], new LoginReturnDTO()); - - } if (loginUser.Status == 0) @@ -712,26 +776,26 @@ namespace IRaCIS.Application.Services if (loginUser.LastChangePassWordTime != null && DateTime.Now.AddDays(-90) > loginUser.LastChangePassWordTime.Value) { loginUser.LoginState = 1; - } + } //登录成功 清除缓存 _cache.Set(cacheKey, 0, TimeSpan.FromMinutes(lockoutMinutes)); - - if (loginUser.LastLoginIP != string.Empty) + + if (loginUser.LastLoginIP != string.Empty) { // 与上一次IP不一致 if (loginUser.LastLoginIP != _userInfo.IP) { - loginUser.LoginState = 2; - } - + loginUser.LoginState = 2; + } - } - await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = loginUser.Id, OptUserId = loginUser.Id, OptType = UserOptType.Login }, true); + } + + await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, LoginUserId = loginUser.Id, OptUserId = loginUser.Id, OptType = UserOptType.Login }, true); userLoginReturnModel.BasicInfo = loginUser; @@ -743,21 +807,13 @@ namespace IRaCIS.Application.Services }); - } + } - await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == loginUser.Id, x => new User() - { - LastLoginIP = _userInfo.IP - - }); - - - // 登录 清除缓存 - //_cache.Remove(userLoginReturnModel.BasicInfo.Id.ToString()); - - var userId = loginUser.Id; - await _cache.SetAsync($"{userId.ToString()}_Online", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TimeSpan.FromMinutes(_verifyConfig.CurrentValue.AutoLoginOutMinutes)); + await _userRepository.BatchUpdateNoTrackingAsync(x => x.Id == loginUser.Id, x => new User() + { + LastLoginIP = _userInfo.IP + }); return ResponseOutput.Ok(userLoginReturnModel); @@ -766,12 +822,12 @@ namespace IRaCIS.Application.Services [HttpPost] public async Task> GetUserLogList(UserLogQuery inQuery) { - DateTime? trialCreateTime = inQuery.TrialId != null ?_repository.Where(t=>t.Id==inQuery.TrialId).Select(t=>t.CreateTime).FirstOrDefault() : null; + DateTime? trialCreateTime = inQuery.TrialId != null ? _repository.Where(t => t.Id == inQuery.TrialId).Select(t => t.CreateTime).FirstOrDefault() : null; var userLogQueryable = _userLogRepository .WhereIf(inQuery.TrialId != null, t => t.LoginUser.UserTrials.Any(c => c.TrialId == inQuery.TrialId && (c.UserId == t.LoginUserId || c.UserId == t.OptUserId))) - .WhereIf(trialCreateTime != null, t => t.CreateTime>= trialCreateTime) + .WhereIf(trialCreateTime != null, t => t.CreateTime >= trialCreateTime) .WhereIf(inQuery.OptType != null, t => t.OptType == inQuery.OptType) .WhereIf(inQuery.UserId != null, t => t.LoginUserId == inQuery.UserId) .WhereIf(inQuery.BeginDate != null, t => t.CreateTime >= inQuery.BeginDate) diff --git a/IRaCIS.Core.Domain/Management/UserLog.cs b/IRaCIS.Core.Domain/Management/UserLog.cs index 631eb51d2..9ed33bfb2 100644 --- a/IRaCIS.Core.Domain/Management/UserLog.cs +++ b/IRaCIS.Core.Domain/Management/UserLog.cs @@ -93,7 +93,11 @@ namespace IRaCIS.Core.Domain.Models DeleteUser=10, - UpdateUser=11 + UpdateUser=11, + + MFALogin=12, + + MFALoginFail=13, } } diff --git a/IRaCIS.Core.Domain/_Config/_AppSettings.cs b/IRaCIS.Core.Domain/_Config/_AppSettings.cs index 0b7018505..679d6d101 100644 --- a/IRaCIS.Core.Domain/_Config/_AppSettings.cs +++ b/IRaCIS.Core.Domain/_Config/_AppSettings.cs @@ -22,6 +22,8 @@ namespace IRaCIS.Core.Domain.Share public int LoginFailLockMinutes { get; set; } public int AutoLoginOutMinutes { get; set; } + + public bool OpenLoginMFA { get; set; } } public class SystemEmailSendConfig