diff --git a/LMS.Common/Password/RandomNumberGenerator.cs b/LMS.Common/Password/RandomNumberGenerator.cs new file mode 100644 index 0000000..4df92ca --- /dev/null +++ b/LMS.Common/Password/RandomNumberGenerator.cs @@ -0,0 +1,78 @@ +using System.Security.Cryptography; +using System.Text; +namespace LMS.Common.Password +{ + public static class PasswordGenerator + { + private const string LowercaseChars = "abcdefghijklmnopqrstuvwxyz"; + private const string UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private const string NumberChars = "0123456789"; + private const string SpecialChars = "@$!%*?.&"; + private const string AllChars = LowercaseChars + UppercaseChars + NumberChars + SpecialChars; + + /// + /// 生成指定长度的随机密码 + /// + /// 密码长度 + /// 随机生成的密码 + public static string GeneratePassword(int length) + { + if (length < 4) + { + throw new ArgumentException("密码长度必须至少为4个字符", nameof(length)); + } + + // 使用加密安全的随机数生成器 + using var rng = RandomNumberGenerator.Create(); + // 确保密码包含至少一个小写字母、一个大写字母、一个数字和一个特殊字符 + var password = new StringBuilder(); + + // 添加每种类型的至少一个字符 + password.Append(GetRandomChar(LowercaseChars, rng)); + password.Append(GetRandomChar(UppercaseChars, rng)); + password.Append(GetRandomChar(NumberChars, rng)); + password.Append(GetRandomChar(SpecialChars, rng)); + + // 添加剩余的随机字符 + for (int i = 4; i < length; i++) + { + password.Append(GetRandomChar(AllChars, rng)); + } + + // 打乱字符顺序 + return ShuffleString(password.ToString(), rng); + } + + /// + /// 从指定字符集中获取一个随机字符 + /// + private static char GetRandomChar(string chars, RandomNumberGenerator rng) + { + byte[] data = new byte[1]; + rng.GetBytes(data); + return chars[data[0] % chars.Length]; + } + + /// + /// 打乱字符串中字符的顺序 + /// + private static string ShuffleString(string input, RandomNumberGenerator rng) + { + char[] array = input.ToCharArray(); + int n = array.Length; + + while (n > 1) + { + byte[] box = new byte[1]; + rng.GetBytes(box); + int k = box[0] % n; + n--; + char temp = array[n]; + array[n] = array[k]; + array[k] = temp; + } + + return new string(array); + } + } +} diff --git a/LMS.Common/Templates/EmailTemplateService.cs b/LMS.Common/Templates/EmailTemplateService.cs index a1d5fb6..ccc465d 100644 --- a/LMS.Common/Templates/EmailTemplateService.cs +++ b/LMS.Common/Templates/EmailTemplateService.cs @@ -9,14 +9,37 @@ -

LMS Registration Code

-

Hi there,

-

Your code is {RegisterCode}

-

The code is valid for 10 minutes, if it is not you, please ignore it.

+

LMS注册验证码

+

您好,

+

您的验证码是 {RegisterCode}

+

该验证码有效期为 10 分钟,如果不是您本人操作,请忽略此邮件。

"""; + public const string ResetPasswordHtmlTemplates = """ + + + +

LMS重置密码验证码

+

您好,

+

您的验证码是 {ResetPasswordCode}

+

该验证码有效期为 10 分钟,如果不是您本人操作,请忽略此邮件。

+ + + """; + + public const string ResetpasswordSuccessMail = """ + + + +

LMS重置密码成功

+

您好,

+

您的重置密码操作已经成功!

+

您的新密码是 {NewPassword}

+ + + """; /// /// 替换模板的占位符 diff --git a/LMS.service/Controllers/UserController.cs b/LMS.service/Controllers/UserController.cs index 24a0cde..131698f 100644 --- a/LMS.service/Controllers/UserController.cs +++ b/LMS.service/Controllers/UserController.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations; +using System.Net.Mail; using static LMS.Common.Enums.ResponseCodeEnum; namespace LMS.service.Controllers @@ -142,6 +143,49 @@ namespace LMS.service.Controllers #endregion + + #region 获取重置密码验证码 + + [HttpPost] + public async Task>> SendResetPasswordCode([FromBody] EmailVerificationService.SendVerificationCodeDto model) + { + if (!ModelState.IsValid) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError); + + return await _loginService.SendResetPasswordCode(model); + } + + #endregion + + #region 用户自行重置密码 + + [HttpPost("{mail}/{code}")] + public async Task>> ResetPassword(string mail, string code) + { + // 校验邮箱是不是有效 + if (string.IsNullOrWhiteSpace(mail)) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱"); + + try + { + // 尝试创建MailAddress实例,如果成功则表示邮箱格式正确 + MailAddress mailAddress = new(mail); + bool isMail = mailAddress.Address == mail; + if (!isMail) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱"); + return await _loginService.UserResetPassword(mail, code); + + } + catch (FormatException) + { + // 创建失败表示邮箱格式不正确 + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "无效的邮箱"); + } + + } + + #endregion + #region 刷新token [HttpPost] @@ -256,7 +300,7 @@ namespace LMS.service.Controllers #endregion - #region 管理员重置用户密码 + #region 重置用户密码 [HttpPost("{id}")] [Authorize] diff --git a/LMS.service/Extensions/Mail/EmailVerificationService.cs b/LMS.service/Extensions/Mail/EmailVerificationService.cs index cb097db..cb82334 100644 --- a/LMS.service/Extensions/Mail/EmailVerificationService.cs +++ b/LMS.service/Extensions/Mail/EmailVerificationService.cs @@ -23,7 +23,13 @@ public class EmailVerificationService public string Email { get; set; } } - // 生成并发送验证码 + #region 注册验证码 + + /// + /// 发送注册验证码 + /// + /// + /// public async Task SendVerificationCodeAsync(string email) { // 生成6位数验证码 @@ -46,7 +52,12 @@ public class EmailVerificationService await _emailService.SendEmailAsync(email, "LMS注册验证码", emailBody); } - // 验证用户提交的验证码 + /// + /// 验证用户提交的验证码 + /// + /// + /// + /// public async Task VerifyCodeAsync(string email, string code) { var storedCode = await _cache.GetStringAsync($"EmailVerification_{email}"); @@ -64,8 +75,84 @@ public class EmailVerificationService return false; } + /// + /// 生成一个6位数的验证码 + /// + /// + private string GenerateVerificationCode() { return _random.Next(100000, 999999).ToString(); } + + #endregion + + #region 重置密码验证码 + + /// + /// 发送重置密码验证码 + /// + /// + /// + public async Task SendResetPasswordCodeAsync(string email) + { + // 生成6位数验证码 + string resetPasswordCode = GenerateVerificationCode(); + // 将验证码保存到分布式缓存,设置10分钟过期 + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) + }; + await _cache.SetStringAsync($"ResetPassword_{email}", resetPasswordCode, options); + var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.ResetPasswordHtmlTemplates, new Dictionary + { + { "ResetPasswordCode", resetPasswordCode } + }); + // 发送验证码邮件 + await _emailService.SendEmailAsync(email, "LMS重置密码验证码", emailBody); + } + + /// + /// 严重用户提交成的重置密码验证码 + /// + /// + /// + /// + public async Task VerifyResetPasswordAsync(string email, string code) + { + var storedCode = await _cache.GetStringAsync($"ResetPassword_{email}"); + + if (string.IsNullOrEmpty(storedCode)) + return false; + + // 使用完就删除验证码 + if (storedCode == code) + { + await _cache.RemoveAsync($"ResetPassword_{email}"); + return true; + } + + return false; + } + #endregion + + + #region 重置密码成功邮件 + + /// + /// 发送重置密码验证码 + /// + /// + /// + public async Task SendResetpasswordSuccessMail(string email, string newPassword) + { + var emailBody = EmailTemplateService.ReplaceTemplate(EmailTemplateService.ResetpasswordSuccessMail, new Dictionary + { + { "NewPassword", newPassword } + }); + // 发送验证码邮件 + await _emailService.SendEmailAsync(email, "LMS重置密码成功", emailBody); + } + + #endregion } \ No newline at end of file diff --git a/LMS.service/Service/MachineService.cs b/LMS.service/Service/MachineService.cs index 50f14c0..19cc4f0 100644 --- a/LMS.service/Service/MachineService.cs +++ b/LMS.service/Service/MachineService.cs @@ -391,8 +391,11 @@ namespace LMS.service.Service query = query.Where(x => filteredOwnerIds.Contains(x.UserID)); } + if (isSuperAdmin) + { - if (isAdmin && !isSuperAdmin) + } + else if (isAdmin) { // 除了超级管理员的代理 其他都能看到 IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); @@ -408,7 +411,7 @@ namespace LMS.service.Service query = query.Where(x => !filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId); } } - else if (isAgent && !isSuperAdmin) + else if (isAgent) { // 代理只能看到自己下面的用户 var list = await userLookupQuery @@ -417,10 +420,18 @@ namespace LMS.service.Service .ToListAsync(); HashSet filteredParentIds = new(list); - if (filteredParentIds?.Count > 0) + if (filteredParentIds != null) { query = query.Where(x => filteredParentIds.Contains(x.UserID) || x.UserID == requestUserId); } + else + { + query = query.Where(x => x.UserID == user.Id); + } + } + else + { + query = query.Where(x => x.UserID == user.Id); } } diff --git a/LMS.service/Service/SoftwareService/SoftwareControlService.cs b/LMS.service/Service/SoftwareService/SoftwareControlService.cs index f3e26e8..cac3ead 100644 --- a/LMS.service/Service/SoftwareService/SoftwareControlService.cs +++ b/LMS.service/Service/SoftwareService/SoftwareControlService.cs @@ -291,7 +291,11 @@ namespace LMS.service.Service.SoftwareService // 做筛选权限 // 获取相关用户ID var userLookupQuery = _userManager.Users.AsNoTracking(); - if (isAdmin && !isSuperAdmin) + if (isSuperAdmin) + { + + } + else if (isAdmin) { // 除了超级管理员的代理 其他都能看到 IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); @@ -307,7 +311,7 @@ namespace LMS.service.Service.SoftwareService query = query.Where(x => !filteredParentIds.Contains(x.UserId) || x.UserId == requestUserId); } } - else if (isAgent && !isSuperAdmin) + else if (isAgent) { // 代理只能看到自己下面的用户 var list = await userLookupQuery @@ -316,12 +320,16 @@ namespace LMS.service.Service.SoftwareService .ToListAsync(); HashSet filteredParentIds = new(list); - if (filteredParentIds?.Count > 0) + if (filteredParentIds != null) { query = query.Where(x => filteredParentIds.Contains(x.UserId)); } + else + { + query = query.Where(x => x.UserId == requestUserId); + } } - else if (!isSuperAdmin) + else { // 普通用户只能看到自己的 query = query.Where(x => x.UserId == requestUserId); diff --git a/LMS.service/Service/UserService/LoginService.cs b/LMS.service/Service/UserService/LoginService.cs index 92c6303..647b7db 100644 --- a/LMS.service/Service/UserService/LoginService.cs +++ b/LMS.service/Service/UserService/LoginService.cs @@ -1,4 +1,6 @@  +using LMS.Common.Enum; +using LMS.Common.Password; using LMS.Common.RSAKey; using LMS.DAO; using LMS.DAO.UserDAO; @@ -15,16 +17,18 @@ using System.Collections.Concurrent; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; +using static Betalgo.Ranul.OpenAI.ObjectModels.SharedModels.IOpenAIModels; using static LMS.Common.Enums.ResponseCodeEnum; namespace LMS.service.Service.UserService { - public class LoginService(UserManager userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao) + public class LoginService(UserManager userManager, ApplicationDbContext context, SecurityService securityService, UserBasicDao userBasicDao, EmailVerificationService emailVerificationService) { private readonly UserManager _userManager = userManager; private readonly ApplicationDbContext _context = context; private readonly SecurityService _securityService = securityService; private readonly UserBasicDao _userBasicDao = userBasicDao; + private readonly EmailVerificationService _verificationService = emailVerificationService; #region 生成JWT /// @@ -417,7 +421,8 @@ namespace LMS.service.Service.UserService } // 检查当前用户是不是超级管理员 bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId); - if (!isSuperAdmin) + // 不是超级管理员,不能重置其他用户的密码 + if (!isSuperAdmin && id != requestUserId) { return APIResponseModel.CreateErrorResponseModel(ResponseCode.NotPermissionAction); } @@ -458,5 +463,97 @@ namespace LMS.service.Service.UserService } } #endregion + + #region 发送重置密码验证码 + internal async Task>> SendResetPasswordCode(EmailVerificationService.SendVerificationCodeDto model) + { + try + { + // 检查邮箱是否已被使用 + var existingUser = await _userManager.FindByEmailAsync(model.Email); + if (existingUser == null) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱未注册,请先注册!"); + + Options? enaleMailService = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.EnableMailService); + if (enaleMailService != null) + { + _ = bool.TryParse(enaleMailService.Value, out bool enableMail); + if (!enableMail) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!"); + } + } + + // 发送验证码 + await _verificationService.SendResetPasswordCodeAsync(model.Email); + return APIResponseModel.CreateSuccessResponseModel(ResponseCode.Success, "验证码发送成功,请在邮箱中查收!"); + } + catch (Exception e) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, e.Message); + } + } + #endregion + #region 用户自行重置密码 + public async Task>> UserResetPassword(string mail, string code) + { + using var transaction = await _context.Database.BeginTransactionAsync(); + try + { + // 检查邮箱是否已被使用 + var existingUser = await _userManager.FindByEmailAsync(mail); + if (existingUser == null) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "当前邮箱未注册,请先注册!"); + // 校验验证码 + Options? enaleMailService = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.EnableMailService); + if (enaleMailService != null) + { + _ = bool.TryParse(enaleMailService.Value, out bool enableMail); + if (!enableMail) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, "邮件服务未开启,无法重置密码!"); + } + } + + var isCodeValid = await _verificationService.VerifyResetPasswordAsync(mail, code); + if (!isCodeValid) + return APIResponseModel.CreateErrorResponseModel(ResponseCode.ParameterError, "验证码无效或已过期"); + + // 生成一个密码 + string randomPassword = PasswordGenerator.GeneratePassword(12); + + // 移除用户当前密码(如果用户没有密码,则需要跳过此步骤) + var hasPassword = await _userManager.HasPasswordAsync(existingUser); + if (hasPassword) + { + var removePasswordResult = await _userManager.RemovePasswordAsync(existingUser); + if (!removePasswordResult.Succeeded) + { + var errors = string.Join("; ", removePasswordResult.Errors.Select(e => e.Description)); + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, $"移除旧密码失败:{errors}"); + } + } + + // 为用户设置新密码 + var addPasswordResult = await _userManager.AddPasswordAsync(existingUser, randomPassword); + if (!addPasswordResult.Succeeded) + { + var errors = string.Join("; ", addPasswordResult.Errors.Select(e => e.Description)); + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, $"重置密码失败:{errors}"); + } + await transaction.CommitAsync(); + + // 重置密码成功,把新密码发送到邮箱 + await _verificationService.SendResetpasswordSuccessMail(mail, randomPassword); + + // 重置密码 + return APIResponseModel.CreateSuccessResponseModel(ResponseCode.Success, "密码重置成功,请去邮箱查看重置密码!"); + } + catch (Exception e) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, e.Message); + } + } + #endregion } } diff --git a/LMS.service/Service/UserService/UserService.cs b/LMS.service/Service/UserService/UserService.cs index 3ca6168..c8835b3 100644 --- a/LMS.service/Service/UserService/UserService.cs +++ b/LMS.service/Service/UserService/UserService.cs @@ -139,7 +139,11 @@ namespace LMS.service.Service.UserService // 获取相关用户ID var userLookupQuery = _userManager.Users.AsNoTracking(); - if (isAdmin && !isSuperAdmin) + if (isSuperAdmin) + { + + } + else if (isAdmin) { // 除了超级管理员的代理 其他都能看到 IList superUsers = await _userManager.GetUsersInRoleAsync("Super Admin"); @@ -155,7 +159,7 @@ namespace LMS.service.Service.UserService query = query.Where(x => !filteredParentIds.Contains(x.Id)); } } - else if (isAgent && !isSuperAdmin) + else if (isAgent) { // 代理只能看到自己下面的用户 var list = await userLookupQuery @@ -164,10 +168,18 @@ namespace LMS.service.Service.UserService .ToListAsync(); HashSet filteredParentIds = new(list); - if (filteredParentIds?.Count > 0) + if (filteredParentIds != null) { query = query.Where(x => filteredParentIds.Contains(x.Id)); } + else + { + query = query.Where(x => x.Id == reuqertUserId); + } + } + else + { + query = query.Where(x => x.Id == reuqertUserId); } diff --git a/LMS.service/appsettings.json b/LMS.service/appsettings.json index d3a6715..9a3b867 100644 --- a/LMS.service/appsettings.json +++ b/LMS.service/appsettings.json @@ -26,6 +26,6 @@ ], "Enrich": [ "FromLogContext" ] }, - "Version": "1.0.4", + "Version": "1.0.5", "AllowedHosts": "*" }