diff --git a/Bootstrap.Admin/wwwroot/js/log.js b/Bootstrap.Admin/wwwroot/js/log.js index 52cab6cf..7f7f477a 100644 --- a/Bootstrap.Admin/wwwroot/js/log.js +++ b/Bootstrap.Admin/wwwroot/js/log.js @@ -30,6 +30,9 @@ }, '#btnSubmitMenu': function () { this.log({ crud: '分配菜单' }); + }, + '#btnReset': function () { + this.log({ crud: '重置密码' }); } } }; diff --git a/Bootstrap.Admin/wwwroot/js/login.js b/Bootstrap.Admin/wwwroot/js/login.js index 23e07d06..7a22d06e 100644 --- a/Bootstrap.Admin/wwwroot/js/login.js +++ b/Bootstrap.Admin/wwwroot/js/login.js @@ -29,7 +29,7 @@ $('#btnForgot').on('click', function () { $.bc({ url: "api/Register", - data: { UserName: $('#f_userName').val(), DisplayName: $('#f_displayName').val(), Description: $('#f_desc').val() }, + data: { UserName: $('#f_userName').val(), DisplayName: $('#f_displayName').val(), Reason: $('#f_desc').val() }, modal: '#dialogForgot', method: "put", callback: function (result) { diff --git a/Bootstrap.Admin/wwwroot/js/users.js b/Bootstrap.Admin/wwwroot/js/users.js index ba5d1637..a351c495 100644 --- a/Bootstrap.Admin/wwwroot/js/users.js +++ b/Bootstrap.Admin/wwwroot/js/users.js @@ -5,8 +5,11 @@ var $dialogGroup = $("#dialogGroup"); var $dialogGroupHeader = $('#myGroupModalLabel'); var $dialogGroupForm = $('#groupForm'); + var $dialogReset = $('#dialogReset'); + var $dialogResetHeader = $('#myResetModalLabel'); + var $table = $('table'); - $('table').lgbTable({ + $table.lgbTable({ url: User.url, dataBinder: { map: { @@ -62,6 +65,9 @@ return $(element).val(); }).toArray(); $.bc({ id: userId, url: Group.url, method: 'put', data: groupIds, query: { type: "user" }, title: Group.title, modal: '#dialogGroup' }); + }, + '#btnReset': function (row) { + $.bc({ id: row.UserName, url: 'api/Register', method: 'put', data: { password: $('#resetPassword').val() }, modal: "#dialogReset", title: "重置密码", callback: function (result) { if (result) $table.bootstrapTable('refresh'); } }); } }, callback: function (data) { @@ -86,7 +92,20 @@ { title: "注册时间", field: "RegisterTime", sortable: true }, { title: "授权时间", field: "ApprovedTime", sortable: true }, { title: "授权人", field: "ApprovedBy", sortable: true }, - { title: "说明", field: "Description", sortable: false } + { title: "说明", field: "Description", sortable: false }, + { + title: "操作", field: "IsReset", formatter: function (value, row, index) { + return value === 1 ? '' : ''; + }, + events: { + 'click .reset': function (e, value, row, index) { + $table.bootstrapTable('uncheckAll'); + $table.bootstrapTable('check', index); + $dialogResetHeader.text($.format("{0} - 重置密码窗口", row.UserName)); + $dialogReset.modal('show'); + } + } + } ] } }); diff --git a/Bootstrap.DataAccess/Helper/UserHelper.cs b/Bootstrap.DataAccess/Helper/UserHelper.cs index e92f5607..7d124a64 100644 --- a/Bootstrap.DataAccess/Helper/UserHelper.cs +++ b/Bootstrap.DataAccess/Helper/UserHelper.cs @@ -124,6 +124,14 @@ namespace Bootstrap.DataAccess /// public static bool ChangePassword(string userName, string password, string newPass) => DbContextManager.Create().ChangePassword(userName, password, newPass); + /// + /// + /// + /// + /// + /// + public static bool ResetPassword(string userName, string password) => DbContextManager.Create().ResetPassword(userName, password); + /// /// /// @@ -131,11 +139,7 @@ namespace Bootstrap.DataAccess /// /// /// - public static bool ForgotPassword(string userName, string displayName, string desc) - { - // UNDONE 忘记密码涉及到安全问题,防止用户恶意重置其他用户,待定 - return true; - } + public static bool ForgotPassword(string userName, string displayName, string desc) => DbContextManager.Create().ForgotPassword(userName, displayName, desc); /// /// @@ -236,5 +240,12 @@ namespace Bootstrap.DataAccess /// /// public static BootstrapUser RetrieveUserByUserName(string userName) => CacheManager.GetOrAdd(string.Format("{0}-{1}", RetrieveUsersByNameDataKey, userName), k => DbContextManager.Create().RetrieveUserByUserName(userName), RetrieveUsersByNameDataKey); + + /// + /// 通过登录账号获得用户信息 + /// + /// + /// + public static ResetUser RetrieveResetUserByUserName(string userName) => DbContextManager.Create().RetrieveUserByUserName(userName); } } diff --git a/Bootstrap.DataAccess/ResetUsers.cs b/Bootstrap.DataAccess/ResetUsers.cs new file mode 100644 index 00000000..4b3b3d58 --- /dev/null +++ b/Bootstrap.DataAccess/ResetUsers.cs @@ -0,0 +1,62 @@ +using PetaPoco; +using System; + +namespace Bootstrap.DataAccess +{ + /// + /// + /// + [TableName("ResetUsers")] + public class ResetUser + { + /// + /// 获得/设置 用户主键ID + /// + public string Id { get; set; } + + /// + /// + /// + public string UserName { get; set; } + + /// + /// + /// + public string DisplayName { get; set; } + + /// + /// + /// + public string Reason { get; set; } + + /// + /// + /// + public DateTime ResetTime { get; set; } + + /// + /// + /// + /// + public virtual bool Save() + { + var db = DbManager.Create(); + db.Save(this); + return true; + } + + /// + /// + /// + /// + /// + public virtual ResetUser RetrieveUserByUserName(string userName) => DbManager.Create().FirstOrDefault("where UserName = @0 order by ResetTime desc", userName); + + /// + /// + /// + /// + /// + public virtual void DeleteByUserName(string userName) => DbManager.Create().Delete("where UserName = @0", userName); + } +} diff --git a/Bootstrap.DataAccess/User.cs b/Bootstrap.DataAccess/User.cs index 5beed944..039af467 100644 --- a/Bootstrap.DataAccess/User.cs +++ b/Bootstrap.DataAccess/User.cs @@ -2,6 +2,7 @@ using Bootstrap.Security.DataAccess; using Longbow.Data; using Longbow.Security.Cryptography; +using PetaPoco; using System; using System.Collections.Generic; using System.Linq; @@ -68,6 +69,12 @@ namespace Bootstrap.DataAccess /// public string NewPassword { get; set; } + /// + /// 获得/设置 是否重置密码 + /// + [Ignore] + public int IsReset { get; set; } + /// /// 验证用户登陆账号与密码正确 /// @@ -114,7 +121,7 @@ namespace Bootstrap.DataAccess /// /// /// - public virtual IEnumerable Retrieves() => DbManager.Create().Fetch("select ID, UserName, DisplayName, RegisterTime, ApprovedTime, ApprovedBy, Description from Users Where ApprovedTime is not null"); + public virtual IEnumerable Retrieves() => DbManager.Create().Fetch("select u.ID, u.UserName, u.DisplayName, RegisterTime, ApprovedTime, ApprovedBy, Description, ru.IsReset from Users u left join (select 1 as IsReset, UserName from ResetUsers group by UserName) ru on u.UserName = ru.UserName Where ApprovedTime is not null"); /// /// 查询所有的新注册用户 @@ -149,6 +156,56 @@ namespace Bootstrap.DataAccess return ret; } + /// + /// + /// + /// + /// + /// + public virtual bool ResetPassword(string userName, string password) + { + var ret = false; + var resetUser = UserHelper.RetrieveResetUserByUserName(userName); + if (resetUser == null) return ret; + + string sql = "set Password = @0, PassSalt = @1 where UserName = @2"; + var passSalt = LgbCryptography.GenerateSalt(); + var newPassword = LgbCryptography.ComputeHash(password, passSalt); + var db = DbManager.Create(); + try + { + db.BeginTransaction(); + ret = db.Update(sql, newPassword, passSalt, userName) == 1; + if (ret) db.Execute("delete from ResetUsers where UserName = @0", userName); + db.CompleteTransaction(); + } + catch (Exception ex) + { + db.AbortTransaction(); + throw ex; + } + return ret; + } + + /// + /// + /// + /// + /// + /// + /// + public virtual bool ForgotPassword(string userName, string displayName, string desc) + { + ResetUser user = new ResetUser() + { + UserName = userName, + DisplayName = displayName, + Reason = desc, + ResetTime = DateTime.Now + }; + return user.Save(); + } + /// /// 新建前台User View调用/注册用户调用 /// diff --git a/DatabaseScripts/Install.sql b/DatabaseScripts/Install.sql index 2c7a3bdd..dbe701b2 100644 --- a/DatabaseScripts/Install.sql +++ b/DatabaseScripts/Install.sql @@ -507,3 +507,30 @@ GO SET ANSI_PADDING OFF GO + +/****** Object: Table [dbo].[ResetUsers] Script Date: 03/05/2019 12:28:32 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +SET ANSI_PADDING ON +GO + +CREATE TABLE [dbo].[ResetUsers]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [UserName] [varchar](50) NOT NULL, + [DisplayName] [nvarchar](50) NOT NULL, + [Reason] [nvarchar](500) NOT NULL, + [ResetTime] [datetime] NOT NULL, + CONSTRAINT [PK_ResetUsers] PRIMARY KEY CLUSTERED +( + [Id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] + +GO + +SET ANSI_PADDING OFF +GO diff --git a/UnitTest/BAWebHost.cs b/UnitTest/BAWebHost.cs index 6b1489fa..c6426eee 100644 --- a/UnitTest/BAWebHost.cs +++ b/UnitTest/BAWebHost.cs @@ -76,10 +76,15 @@ namespace Bootstrap.Admin public HttpClient CreateClient(string baseAddress) { var client = CreateDefaultClient(new Uri($"http://localhost/{baseAddress}/"), new RedirectHandler(7), new CookieContainerHandler(_cookie)); - client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15"); return client; } + protected override void ConfigureClient(HttpClient client) + { + base.ConfigureClient(client); + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15"); + } + private readonly CookieContainer _cookie = new CookieContainer(); protected override void ConfigureWebHost(IWebHostBuilder builder) diff --git a/UnitTest/Bootstrap.Admin/Api/RegisterTest.cs b/UnitTest/Bootstrap.Admin/Api/RegisterTest.cs index 22816202..5adb6178 100644 --- a/UnitTest/Bootstrap.Admin/Api/RegisterTest.cs +++ b/UnitTest/Bootstrap.Admin/Api/RegisterTest.cs @@ -25,5 +25,26 @@ namespace Bootstrap.Admin.Api nusr.Delete(nusr.RetrieveNewUsers().Where(u => u.UserName == nusr.UserName).Select(u => u.Id)); } + + [Fact] + public async void Put_Ok() + { + var user = new ResetUser() { DisplayName = "UnitTest", UserName = "UnitTest", Reason = "UnitTest" }; + var resp = await Client.PutAsJsonAsync(user); + Assert.True(resp); + } + + [Fact] + public async void Put_UserName() + { + var user = new User() { Password = "1" }; + var resp = await Client.PutAsJsonAsync("UnitTest", user); + Assert.False(resp); + + // 重置Admin密码 + await Client.PutAsJsonAsync(new ResetUser { UserName = "Admin", DisplayName = "Administrator", Reason = "UnitTest" }); + resp = await Client.PutAsJsonAsync("Admin", new User() { Password = "123789" }); + Assert.True(resp); + } } } diff --git a/UnitTest/Bootstrap.DataAccess/ResetUserTest.cs b/UnitTest/Bootstrap.DataAccess/ResetUserTest.cs new file mode 100644 index 00000000..b2597544 --- /dev/null +++ b/UnitTest/Bootstrap.DataAccess/ResetUserTest.cs @@ -0,0 +1,61 @@ +using System; +using Xunit; + +namespace Bootstrap.DataAccess +{ + [Collection("SQLServerContext")] + public class ResetUserTest + { + [Fact] + public void Save_Ok() + { + var resetUser = new ResetUser() + { + UserName = "UnitTest", + Reason = "UnitTest", + DisplayName = "UnitTest", + ResetTime = DateTime.Now + }; + var db = DbManager.Create(); + db.Save(resetUser); + var count = db.ExecuteScalar("select count(Id) from ResetUsers"); + Assert.True(count > 0); + } + + [Fact] + public void RetrieveUserByUserName_Ok() + { + var resetUser = new ResetUser() + { + UserName = "UnitTest", + Reason = "UnitTest", + DisplayName = "UnitTest", + ResetTime = DateTime.Now + }; + var db = DbManager.Create(); + db.Save(resetUser); + + var user = resetUser.RetrieveUserByUserName(resetUser.UserName); + Assert.Equal("UnitTest", user.UserName); + Assert.Equal("UnitTest", user.DisplayName); + } + + [Fact] + public void DeleteByUserName_Ok() + { + var resetUser = new ResetUser() + { + UserName = "UnitTest", + Reason = "UnitTest", + DisplayName = "UnitTest", + ResetTime = DateTime.Now + }; + var db = DbManager.Create(); + db.Save(resetUser); + + resetUser.DeleteByUserName(resetUser.UserName); + var count = db.ExecuteScalar("select count(Id) from ResetUsers"); + Assert.Equal(0, count); + } + } +}