From ea8dd31e88aaf13af1f51e764623d6a7c73fb429 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 13:41:46 +0800 Subject: ... --- Timeline.Tests/Controllers/UserControllerTest.cs | 109 +++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 Timeline.Tests/Controllers/UserControllerTest.cs (limited to 'Timeline.Tests/Controllers/UserControllerTest.cs') diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs new file mode 100644 index 00000000..9fec477f --- /dev/null +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -0,0 +1,109 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; +using Timeline.Tests.Mock.Services; +using Xunit; +using static Timeline.ErrorCodes.Http.User; + +namespace Timeline.Tests.Controllers +{ + public class UserControllerTest : IDisposable + { + private readonly Mock _mockUserService = new Mock(); + + private readonly UserController _controller; + + public UserControllerTest() + { + _controller = new UserController(NullLogger.Instance, + _mockUserService.Object, + TestStringLocalizerFactory.Create()); + } + + public void Dispose() + { + _controller.Dispose(); + } + + [Fact] + public async Task GetList_Success() + { + var array = MockUser.UserInfoList.ToArray(); + _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(array); + var action = await _controller.List(); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeEquivalentTo(array); + } + + [Fact] + public async Task Get_Success() + { + const string username = "aaa"; + _mockUserService.Setup(s => s.GetUser(username)).ReturnsAsync(MockUser.User.Info); + var action = await _controller.Get(username); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeEquivalentTo(MockUser.User.Info); + } + + [Fact] + public async Task Get_NotFound() + { + const string username = "aaa"; + _mockUserService.Setup(s => s.GetUser(username)).Returns(Task.FromResult(null)); + var action = await _controller.Get(username); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Get.NotExist); + } + + [Theory] + [InlineData(PutResult.Created, true)] + [InlineData(PutResult.Modified, false)] + public async Task Put_Success(PutResult result, bool create) + { + const string username = "aaa"; + const string password = "ppp"; + const bool administrator = true; + _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ReturnsAsync(result); + var action = await _controller.Put(new UserPutRequest + { + Password = password, + Administrator = administrator + }, username); + var response = action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which; + response.Code.Should().Be(0); + response.Data.Create.Should().Be(create); + } + + [Fact] + public async Task Put_BadUsername() + { + const string username = "aaa"; + const string password = "ppp"; + const bool administrator = true; + _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ThrowsAsync(new UsernameBadFormatException()); + var action = await _controller.Put(new UserPutRequest + { + Password = password, + Administrator = administrator + }, username); + action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Put.BadUsername); + } + + //TODO! Complete this. + } +} -- cgit v1.2.3 From 160f933a38a1c4b82df2038ab2446acde873ff8f Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 17:38:35 +0800 Subject: ... --- Timeline.Tests/Controllers/UserControllerTest.cs | 132 +++++++++++++++- Timeline.Tests/IntegratedTests/UserUnitTest.cs | 188 +++++++++-------------- 2 files changed, 207 insertions(+), 113 deletions(-) (limited to 'Timeline.Tests/Controllers/UserControllerTest.cs') diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 9fec477f..ddbc3fbc 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -1,9 +1,11 @@ using FluentAssertions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; using Moq; using System; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Timeline.Controllers; using Timeline.Models; @@ -104,6 +106,134 @@ namespace Timeline.Tests.Controllers .Which.Code.Should().Be(Put.BadUsername); } - //TODO! Complete this. + [Fact] + public async Task Patch_Success() + { + const string username = "aaa"; + const string password = "ppp"; + const bool administrator = true; + _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).Returns(Task.CompletedTask); + var action = await _controller.Patch(new UserPatchRequest + { + Password = password, + Administrator = administrator + }, username); + action.Should().BeAssignableTo(); + } + + [Fact] + public async Task Patch_NotExist() + { + const string username = "aaa"; + const string password = "ppp"; + const bool administrator = true; + _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).ThrowsAsync(new UserNotExistException()); + var action = await _controller.Patch(new UserPatchRequest + { + Password = password, + Administrator = administrator + }, username); + action.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Patch.NotExist); + } + + [Fact] + public async Task Delete_Delete() + { + const string username = "aaa"; + _mockUserService.Setup(s => s.DeleteUser(username)).Returns(Task.CompletedTask); + var action = await _controller.Delete(username); + var body = action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().BeTrue(); + } + + [Fact] + public async Task Delete_NotExist() + { + const string username = "aaa"; + _mockUserService.Setup(s => s.DeleteUser(username)).ThrowsAsync(new UserNotExistException()); + var action = await _controller.Delete(username); + var body = action.Result.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().BeFalse(); + } + + [Fact] + public async Task Op_ChangeUsername_Success() + { + const string oldUsername = "aaa"; + const string newUsername = "bbb"; + _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).Returns(Task.CompletedTask); + var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }); + action.Should().BeAssignableTo(); + } + + [Theory] + [InlineData(typeof(UserNotExistException), Op.ChangeUsername.NotExist)] + [InlineData(typeof(UserAlreadyExistException), Op.ChangeUsername.AlreadyExist)] + public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) + { + const string oldUsername = "aaa"; + const string newUsername = "bbb"; + _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception); + var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }); + action.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(code); + } + + [Fact] + public async Task Op_ChangePassword_Success() + { + const string username = "aaa"; + const string oldPassword = "aaa"; + const string newPassword = "bbb"; + _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).Returns(Task.CompletedTask); + + _controller.ControllerContext = new ControllerContext() + { + HttpContext = new DefaultHttpContext() + { + User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.Name, username) + }, "TestAuthType")) + } + }; + + var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + action.Should().BeAssignableTo(); + } + + [Fact] + public async Task Op_ChangePassword_BadPassword() + { + const string username = "aaa"; + const string oldPassword = "aaa"; + const string newPassword = "bbb"; + _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).ThrowsAsync(new BadPasswordException()); + + _controller.ControllerContext = new ControllerContext() + { + HttpContext = new DefaultHttpContext() + { + User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.Name, username) + }, "TestAuthType")) + } + }; + + var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + action.Should().BeAssignableTo() + .Which.Value.Should().BeAssignableTo() + .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); + } } } diff --git a/Timeline.Tests/IntegratedTests/UserUnitTest.cs b/Timeline.Tests/IntegratedTests/UserUnitTest.cs index 47a8699c..b00648de 100644 --- a/Timeline.Tests/IntegratedTests/UserUnitTest.cs +++ b/Timeline.Tests/IntegratedTests/UserUnitTest.cs @@ -170,131 +170,95 @@ namespace Timeline.Tests.IntegratedTests res.Should().BeDelete(false); } + private const string changeUsernameUrl = "userop/changeusername"; - public class ChangeUsernameUnitTest : IClassFixture>, IDisposable + public static IEnumerable Op_ChangeUsername_InvalidModel_Data() { - private const string url = "userop/changeusername"; - - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public ChangeUsernameUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - public static IEnumerable InvalidModel_Data() - { - yield return new[] { null, "uuu" }; - yield return new[] { "uuu", null }; - yield return new[] { "uuu", "???" }; - } - - [Theory] - [MemberData(nameof(InvalidModel_Data))] - public async Task InvalidModel(string oldUsername, string newUsername) - { - using var client = await _factory.CreateClientAsAdmin(); - (await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) - .Should().BeInvalidModel(); - } - - [Fact] - public async Task UserNotExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.NotExist); - } - - [Fact] - public async Task UserAlreadyExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); - } - - [Fact] - public async Task Success() - { - using var client = await _factory.CreateClientAsAdmin(); - const string newUsername = "hahaha"; - var res = await client.PostAsJsonAsync(url, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); - res.Should().HaveStatusCode(200); - await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); - } + yield return new[] { null, "uuu" }; + yield return new[] { "uuu", null }; + yield return new[] { "uuu", "???" }; } + [Theory] + [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))] + public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername) + { + using var client = await _factory.CreateClientAsAdmin(); + (await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) + .Should().BeInvalidModel(); + } - public class ChangePasswordUnitTest : IClassFixture>, IDisposable + [Fact] + public async Task Op_ChangeUsername_UserNotExist() { - private const string url = "userop/changepassword"; + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangeUsername.NotExist); + } - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; + [Fact] + public async Task Op_ChangeUsername_UserAlreadyExist() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); + } - public ChangePasswordUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } + [Fact] + public async Task Op_ChangeUsername_Success() + { + using var client = await _factory.CreateClientAsAdmin(); + const string newUsername = "hahaha"; + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); + res.Should().HaveStatusCode(200); + await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); + } - public void Dispose() - { - _testApp.Dispose(); - } + private const string changePasswordUrl = "userop/changepassword"; - public static IEnumerable InvalidModel_Data() - { - yield return new[] { null, "ppp" }; - yield return new[] { "ppp", null }; - } + public static IEnumerable Op_ChangePassword_InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } - [Theory] - [MemberData(nameof(InvalidModel_Data))] - public async Task InvalidModel(string oldPassword, string newPassword) - { - using var client = await _factory.CreateClientAsUser(); - (await client.PostAsJsonAsync(url, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) - .Should().BeInvalidModel(); - } + [Theory] + [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] + public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) + { + using var client = await _factory.CreateClientAsUser(); + (await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) + .Should().BeInvalidModel(); + } - [Fact] - public async Task BadOldPassword() - { - using var client = await _factory.CreateClientAsUser(); - var res = await client.PostAsJsonAsync(url, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); - } + [Fact] + public async Task Op_ChangePassword_BadOldPassword() + { + using var client = await _factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); + } - [Fact] - public async Task Success() - { - using var client = await _factory.CreateClientAsUser(); - const string newPassword = "new"; - var res = await client.PostAsJsonAsync(url, - new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); - res.Should().HaveStatusCode(200); - await client.CreateUserTokenAsync(MockUser.User.Username, newPassword); - } + [Fact] + public async Task Op_ChangePassword_Success() + { + using var client = await _factory.CreateClientAsUser(); + const string newPassword = "new"; + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); + res.Should().HaveStatusCode(200); + await client.CreateUserTokenAsync(MockUser.User.Username, newPassword); } } } -- cgit v1.2.3 From ec7dfb73ace61a1aba5156cc1048cbe32ee1cee6 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 21 Oct 2019 20:47:31 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 6 +- Timeline.Tests/Controllers/UserControllerTest.cs | 6 +- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 145 ----------- Timeline.Tests/UserDetailServiceTest.cs | 271 --------------------- Timeline.Tests/UserDetailValidatorTest.cs | 97 -------- Timeline.Tests/UsernameValidatorUnitTest.cs | 7 +- Timeline/Controllers/TokenController.cs | 38 +-- Timeline/Controllers/UserController.cs | 31 +-- Timeline/Controllers/UserDetailController.cs | 96 -------- Timeline/Models/Http/Common.cs | 8 +- Timeline/Models/PutResult.cs | 4 +- Timeline/Models/UserDetail.cs | 45 ---- Timeline/Models/Validation/UserDetailValidator.cs | 116 --------- Timeline/Models/Validation/UsernameValidator.cs | 5 +- Timeline/Models/Validation/Validator.cs | 44 ++-- .../Controllers/TokenController.Designer.cs | 153 ++++++++++++ .../Controllers/UserController.Designer.cs | 171 +++++++++++++ Timeline/Resources/Http/Common.en.resx | 132 ---------- Timeline/Resources/Http/Common.zh.resx | 132 ---------- Timeline/Resources/Models/Http/Common.en.resx | 132 ++++++++++ Timeline/Resources/Models/Http/Common.zh.resx | 132 ++++++++++ .../Models/Validation/Validator.Designer.cs | 81 ++++++ .../Resources/Models/Validation/Validator.en.resx | 129 ++++++++++ .../Resources/Models/Validation/Validator.resx | 126 ++++++++++ .../Resources/Models/Validation/Validator.zh.resx | 129 ++++++++++ Timeline/Resources/Services/Exception.Designer.cs | 189 ++++++++++++++ Timeline/Resources/Services/Exception.resx | 162 ++++++++++++ Timeline/Services/BadPasswordException.cs | 27 ++ Timeline/Services/JwtBadVersionException.cs | 36 +++ Timeline/Services/JwtService.cs | 76 +----- Timeline/Services/JwtVerifyException.cs | 59 +++++ Timeline/Services/UserDetailService.cs | 135 ---------- Timeline/Services/UserNotExistException.cs | 41 ++++ Timeline/Services/UserService.cs | 195 ++------------- Timeline/Services/UsernameBadFormatException.cs | 27 ++ Timeline/Services/UsernameConfictException.cs | 25 ++ Timeline/Startup.cs | 1 - Timeline/Timeline.csproj | 34 ++- 38 files changed, 1768 insertions(+), 1475 deletions(-) delete mode 100644 Timeline.Tests/IntegratedTests/UserDetailTest.cs delete mode 100644 Timeline.Tests/UserDetailServiceTest.cs delete mode 100644 Timeline.Tests/UserDetailValidatorTest.cs delete mode 100644 Timeline/Controllers/UserDetailController.cs delete mode 100644 Timeline/Models/UserDetail.cs delete mode 100644 Timeline/Models/Validation/UserDetailValidator.cs create mode 100644 Timeline/Resources/Controllers/TokenController.Designer.cs create mode 100644 Timeline/Resources/Controllers/UserController.Designer.cs delete mode 100644 Timeline/Resources/Http/Common.en.resx delete mode 100644 Timeline/Resources/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Http/Common.en.resx create mode 100644 Timeline/Resources/Models/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/Validator.en.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.resx create mode 100644 Timeline/Resources/Models/Validation/Validator.zh.resx create mode 100644 Timeline/Resources/Services/Exception.Designer.cs create mode 100644 Timeline/Resources/Services/Exception.resx create mode 100644 Timeline/Services/BadPasswordException.cs create mode 100644 Timeline/Services/JwtBadVersionException.cs create mode 100644 Timeline/Services/JwtVerifyException.cs delete mode 100644 Timeline/Services/UserDetailService.cs create mode 100644 Timeline/Services/UserNotExistException.cs create mode 100644 Timeline/Services/UsernameBadFormatException.cs create mode 100644 Timeline/Services/UsernameConfictException.cs (limited to 'Timeline.Tests/Controllers/UserControllerTest.cs') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 71520e77..53b6c606 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -101,9 +101,9 @@ namespace Timeline.Tests.Controllers public static IEnumerable Verify_BadRequest_Data() { - yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.Expired), Verify.Expired }; - yield return new object[] { new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; - yield return new object[] { new BadTokenVersionException(), Verify.OldVersion }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), Verify.Expired }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), Verify.BadFormat }; + yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), Verify.OldVersion }; yield return new object[] { new UserNotExistException(), Verify.UserNotExist }; } diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index ddbc3fbc..471ed851 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -69,8 +69,8 @@ namespace Timeline.Tests.Controllers } [Theory] - [InlineData(PutResult.Created, true)] - [InlineData(PutResult.Modified, false)] + [InlineData(PutResult.Create, true)] + [InlineData(PutResult.Modify, false)] public async Task Put_Success(PutResult result, bool create) { const string username = "aaa"; @@ -176,7 +176,7 @@ namespace Timeline.Tests.Controllers [Theory] [InlineData(typeof(UserNotExistException), Op.ChangeUsername.NotExist)] - [InlineData(typeof(UserAlreadyExistException), Op.ChangeUsername.AlreadyExist)] + [InlineData(typeof(UsernameConfictException), Op.ChangeUsername.AlreadyExist)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs deleted file mode 100644 index 4d268efa..00000000 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ /dev/null @@ -1,145 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Models; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserDetailTest : IClassFixture>, IDisposable - { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public UserDetailTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - [Fact] - public async Task TestAsUser() - { - using (var client = await _factory.CreateClientAsUser()) - { - { - var res = await client.GetAsync($"users/usernotexist/nickname"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.GetNickname_UserNotExist); - } - - { - var res = await client.GetAsync($"users/usernotexist/details"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Get_UserNotExist); - } - - async Task GetAndTest(UserDetail d) - { - var res = await client.GetAsync($"users/{MockUser.User.Username}/details"); - res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(d); - } - - await GetAndTest(new UserDetail()); - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.Admin.Username}/details", new UserDetail()); - res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_Forbid); - } - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail - { - Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - QQ = "aaaaaaa", - Email = "aaaaaa", - PhoneNumber = "aaaaaaaa" - }); - var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which; - body.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel); - foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" }) - { - body.Message.Should().ContainEquivalentOf(key); - } - } - - - var detail = new UserDetail - { - Nickname = "aaa", - QQ = "1234567", - Email = "aaaa@aaa.net", - Description = "aaaaaaaaa" - }; - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail); - res.Should().HaveStatusCode(200); - await GetAndTest(detail); - } - - { - var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); - res.Should().HaveStatusCode(200).And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(new UserDetail - { - Nickname = detail.Nickname - }); - } - - var detail2 = new UserDetail - { - QQ = "", - PhoneNumber = "12345678910", - Description = "bbbbbbbb" - }; - - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", detail2); - res.Should().HaveStatusCode(200); - await GetAndTest(new UserDetail - { - Nickname = detail.Nickname, - QQ = null, - Email = detail.Email, - PhoneNumber = detail2.PhoneNumber, - Description = detail2.Description - }); - } - } - } - - [Fact] - public async Task TestAsAdmin() - { - using (var client = await _factory.CreateClientAsAdmin()) - { - { - var res = await client.PatchAsJsonAsync($"users/{MockUser.User.Username}/details", new UserDetail()); - res.Should().HaveStatusCode(200); - } - - { - var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail()); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody().Which.Code.Should().Be(UserDetailController.ErrorCodes.Patch_UserNotExist); - } - } - } - } -} \ No newline at end of file diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs deleted file mode 100644 index d16d1a40..00000000 --- a/Timeline.Tests/UserDetailServiceTest.cs +++ /dev/null @@ -1,271 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; -using Xunit; - -namespace Timeline.Tests -{ - public class UserDetailServiceTest : IDisposable - { - private readonly TestDatabase _database; - - private readonly UserDetailService _service; - - public UserDetailServiceTest() - { - _database = new TestDatabase(); - - _service = new UserDetailService(NullLogger.Instance, _database.DatabaseContext); - } - - public void Dispose() - { - _database.Dispose(); - } - - [Fact] - public void GetNickname_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetUserNickname(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetUserNickname("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void GetNickname_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetUserNickname(username)).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task GetNickname_Should_Create_And_ReturnDefault() - { - { - var nickname = await _service.GetUserNickname(MockUser.User.Username); - nickname.Should().BeNull(); - } - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); - detail.Nickname.Should().BeNullOrEmpty(); - detail.QQ.Should().BeNullOrEmpty(); - detail.Email.Should().BeNullOrEmpty(); - detail.PhoneNumber.Should().BeNullOrEmpty(); - detail.Description.Should().BeNullOrEmpty(); - } - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("nickname")] - public async Task GetNickname_Should_ReturnData(string nickname) - { - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = new UserDetailEntity - { - Nickname = nickname, - UserId = userId - }; - context.Add(entity); - await context.SaveChangesAsync(); - } - - { - var n = await _service.GetUserNickname(MockUser.User.Username); - n.Should().Equals(string.IsNullOrEmpty(nickname) ? null : nickname); - } - } - - [Fact] - public void GetDetail_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.GetUserDetail(null)).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.GetUserDetail("")).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - } - - [Fact] - public void GetDetail_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.GetUserDetail(username)).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task GetDetail_Should_Create_And_ReturnDefault() - { - { - var detail = await _service.GetUserDetail(MockUser.User.Username); - detail.Should().BeEquivalentTo(new UserDetail()); - } - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); - detail.Nickname.Should().BeNullOrEmpty(); - detail.QQ.Should().BeNullOrEmpty(); - detail.Email.Should().BeNullOrEmpty(); - detail.PhoneNumber.Should().BeNullOrEmpty(); - detail.Description.Should().BeNullOrEmpty(); - } - } - - [Fact] - public async Task GetDetail_Should_ReturnData() - { - const string email = "ha@aaa.net"; - const string description = "hahaha"; - - - { - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = new UserDetailEntity - { - Email = email, - Description = description, - UserId = userId - }; - context.Add(entity); - await context.SaveChangesAsync(); - } - - { - var detail = await _service.GetUserDetail(MockUser.User.Username); - detail.Should().BeEquivalentTo(new UserDetail - { - Email = email, - Description = description - }); - } - } - - [Fact] - public void UpdateDetail_ShouldThrow_ArgumentException() - { - // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.UpdateUserDetail(null, new UserDetail())).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw() - .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw() - .Where(e => e.ParamName == "detail"); - } - - [Fact] - public void UpdateDetail_ShouldThrow_UserNotExistException() - { - const string username = "usernotexist"; - _service.Awaiting(s => s.UpdateUserDetail(username, new UserDetail())).Should().Throw() - .Where(e => e.Username == username); - } - - [Fact] - public async Task UpdateDetail_Empty_Should_Work() - { - await _service.UpdateUserDetail(MockUser.User.Username, new UserDetail()); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - entity.Nickname.Should().BeNullOrEmpty(); - entity.QQ.Should().BeNullOrEmpty(); - entity.Email.Should().BeNullOrEmpty(); - entity.PhoneNumber.Should().BeNullOrEmpty(); - entity.Description.Should().BeNullOrEmpty(); - } - - [Theory] - [InlineData(nameof(UserDetail.Nickname), nameof(UserDetailEntity.Nickname), "aaaa", "bbbb")] - [InlineData(nameof(UserDetail.QQ), nameof(UserDetailEntity.QQ), "12345678910", "987654321")] - [InlineData(nameof(UserDetail.Email), nameof(UserDetailEntity.Email), "aaa@aaa.aaa", "bbb@bbb.bbb")] - [InlineData(nameof(UserDetail.PhoneNumber), nameof(UserDetailEntity.PhoneNumber), "12345678910", "987654321")] - [InlineData(nameof(UserDetail.Description), nameof(UserDetailEntity.Description), "descriptionA", "descriptionB")] - public async Task UpdateDetail_Single_Should_Work(string propertyName, string entityPropertyName, string mockData1, string mockData2) - { - - UserDetail CreateWith(string propertyValue) - { - var detail = new UserDetail(); - typeof(UserDetail).GetProperty(propertyName).SetValue(detail, propertyValue); - return detail; - } - - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData1)); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - - void TestWith(string propertyValue) - { - typeof(UserDetailEntity).GetProperty(entityPropertyName).GetValue(entity).Should().Equals(propertyValue); - foreach (var p in typeof(UserDetailEntity).GetProperties().Where(p => p.Name != entityPropertyName)) - (p.GetValue(entity) as string).Should().BeNullOrEmpty(); - } - - TestWith(mockData1); - - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith(mockData2)); - TestWith(mockData2); - await _service.UpdateUserDetail(MockUser.User.Username, CreateWith("")); - TestWith(""); - } - - [Fact] - public async Task UpdateDetail_Multiple_Should_Work() - { - var detail = new UserDetail - { - QQ = "12345678", - Email = "aaa@aaa.aaa", - PhoneNumber = "11111111111", - Description = "aaaaaaaaaa" - }; - - await _service.UpdateUserDetail(MockUser.User.Username, detail); - - var context = _database.DatabaseContext; - var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUser.User.Username); - var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); - entity.QQ.Should().Equals(detail.QQ); - entity.Email.Should().Equals(detail.Email); - entity.PhoneNumber.Should().Equals(detail.PhoneNumber); - entity.Description.Should().Equals(detail.Description); - - var detail2 = new UserDetail - { - QQ = null, - Email = "bbb@bbb.bbb", - PhoneNumber = "", - Description = "bbbbbbbbb" - }; - - await _service.UpdateUserDetail(MockUser.User.Username, detail2); - entity.QQ.Should().Equals(detail.QQ); - entity.Email.Should().Equals(detail2.Email); - entity.PhoneNumber.Should().BeNullOrEmpty(); - entity.Description.Should().Equals(detail2.Description); - } - } -} diff --git a/Timeline.Tests/UserDetailValidatorTest.cs b/Timeline.Tests/UserDetailValidatorTest.cs deleted file mode 100644 index 9b112946..00000000 --- a/Timeline.Tests/UserDetailValidatorTest.cs +++ /dev/null @@ -1,97 +0,0 @@ -using FluentAssertions; -using System.Collections.Generic; -using Timeline.Models.Validation; -using Xunit; - -namespace Timeline.Tests -{ - public static class UserDetailValidatorsTest - { - private static void SucceedWith(object value) where TValidator : class, IValidator, new() - { - var result = new TValidator().Validate(value, out var message); - result.Should().BeTrue(); - message.Should().Equals(ValidationConstants.SuccessMessage); - } - - private static void FailWith(object value, params string[] messageContains) where TValidator : class, IValidator, new() - { - var result = new TValidator().Validate(value, out var message); - result.Should().BeFalse(); - - foreach (var m in messageContains) - { - message.Should().ContainEquivalentOf(m); - } - } - - public class QQ - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("12345678")] - public void Success(object qq) - { - SucceedWith(qq); - } - - [Theory] - [InlineData(123, "type")] - [InlineData("123", "short")] - [InlineData("111111111111111111111111111111111111", "long")] - [InlineData("aaaaaaaa", "digit")] - public void Fail(object qq, string messageContains) - { - FailWith(qq, messageContains); - } - } - - public class EMail - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("aaa@aaa.net")] - public void Success(object email) - { - SucceedWith(email); - } - - public static IEnumerable FailTestData() - { - yield return new object[] { 123, "type" }; - yield return new object[] { new string('a', 100), "long" }; - yield return new object[] { "aaaaaaaa", "format" }; - } - - [Theory] - [MemberData(nameof(FailTestData))] - public void Fail(object email, string messageContains) - { - FailWith(email, messageContains); - } - } - - public class PhoneNumber - { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("12345678910")] - public void Success(object phoneNumber) - { - SucceedWith(phoneNumber); - } - - [Theory] - [InlineData(123, "type")] - [InlineData("111111111111111111111111111111111111", "long")] - [InlineData("aaaaaaaa", "digit")] - public void Fail(object phoneNumber, string messageContains) - { - FailWith(phoneNumber, messageContains); - } - } - } -} diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 6a635ba1..9a80a3a2 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Timeline.Models.Validation; +using Timeline.Tests.Mock.Services; using Xunit; namespace Timeline.Tests @@ -15,14 +16,14 @@ namespace Timeline.Tests private string FailAndMessage(string username) { - var result = _validator.Validate(username, out var message); + var result = _validator.Validate(username, TestStringLocalizerFactory.Create(), out var message); result.Should().BeFalse(); return message; } private void Succeed(string username) { - _validator.Validate(username, out var message).Should().BeTrue(); + _validator.Validate(username, TestStringLocalizerFactory.Create(), out var message).Should().BeTrue(); message.Should().Be(ValidationConstants.SuccessMessage); } @@ -35,7 +36,7 @@ namespace Timeline.Tests [Fact] public void NotString() { - var result = _validator.Validate(123, out var message); + var result = _validator.Validate(123, TestStringLocalizerFactory.Create(), out var message); result.Should().BeFalse(); message.Should().ContainEquivalentOf("type"); } diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index cf32a562..4e32d26f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -9,6 +9,7 @@ using Timeline.Services; using Timeline.Helpers; using Microsoft.Extensions.Localization; using System.Globalization; +using static Timeline.Resources.Controllers.TokenController; namespace Timeline { @@ -60,7 +61,7 @@ namespace Timeline.Controllers { void LogFailure(string reason, Exception? e = null) { - _logger.LogInformation(e, Log.Format(_localizer["LogCreateFailure"], + _logger.LogInformation(e, Log.Format(LogCreateFailure, ("Reason", reason), ("Username", request.Username), ("Password", request.Password), @@ -76,7 +77,7 @@ namespace Timeline.Controllers var result = await _userService.CreateToken(request.Username, request.Password, expireTime); - _logger.LogInformation(Log.Format(_localizer["LogCreateSuccess"], + _logger.LogInformation(Log.Format(LogCreateSuccess, ("Username", request.Username), ("Expire At", expireTime?.ToString(CultureInfo.CurrentUICulture.DateTimeFormat) ?? "default") )); @@ -88,13 +89,13 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - LogFailure(_localizer["LogUserNotExist"], e); + LogFailure(LogUserNotExist, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, _localizer["ErrorBadCredential"])); } catch (BadPasswordException e) { - LogFailure(_localizer["LogBadPassword"], e); + LogFailure(LogBadPassword, e); return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential, _localizer["ErrorBadCredential"])); } @@ -110,49 +111,50 @@ namespace Timeline.Controllers properties[0] = ("Reason", reason); properties[1] = ("Token", request.Token); otherProperties.CopyTo(properties, 2); - _logger.LogInformation(e, Log.Format(_localizer["LogVerifyFailure"], properties)); + _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties)); } try { var result = await _userService.VerifyToken(request.Token); - _logger.LogInformation(Log.Format(_localizer["LogVerifySuccess"], + _logger.LogInformation(Log.Format(LogVerifySuccess, ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { User = result }); } - catch (JwtTokenVerifyException e) + catch (JwtVerifyException e) { - if (e.ErrorCode == JwtTokenVerifyException.ErrorCodes.Expired) + if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired) { var innerException = e.InnerException as SecurityTokenExpiredException; - LogFailure(_localizer["LogVerifyExpire"], e, ("Expires", innerException?.Expires), + LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires), ("Current Time", _clock.GetCurrentTime())); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.Expired, _localizer["ErrorVerifyExpire"])); } + else if (e.ErrorCode == JwtVerifyException.ErrorCodes.OldVersion) + { + var innerException = e.InnerException as JwtBadVersionException; + LogFailure(LogVerifyOldVersion, e, + ("Token Version", innerException?.TokenVersion), ("Required Version", innerException?.RequiredVersion)); + return BadRequest(new CommonResponse( + ErrorCodes.Http.Token.Verify.OldVersion, _localizer["ErrorVerifyOldVersion"])); + } else { - LogFailure(_localizer["LogVerifyBadFormat"], e); + LogFailure(LogVerifyBadFormat, e); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.BadFormat, _localizer["ErrorVerifyBadFormat"])); } } catch (UserNotExistException e) { - LogFailure(_localizer["LogVerifyUserNotExist"], e); + LogFailure(LogVerifyUserNotExist, e); return BadRequest(new CommonResponse( ErrorCodes.Http.Token.Verify.UserNotExist, _localizer["ErrorVerifyUserNotExist"])); } - catch (BadTokenVersionException e) - { - LogFailure(_localizer["LogVerifyOldVersion"], e, - ("Token Version", e.TokenVersion), ("Required Version", e.RequiredVersion)); - return BadRequest(new CommonResponse( - ErrorCodes.Http.Token.Verify.OldVersion, _localizer["ErrorVerifyOldVersion"])); - } } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 6afc890c..b8d1d659 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; +using static Timeline.Resources.Controllers.UserController; namespace Timeline { @@ -82,7 +83,7 @@ namespace Timeline.Controllers var user = await _userService.GetUser(username); if (user == null) { - _logger.LogInformation(Log.Format(_localizer["LogGetUserNotExist"], ("Username", username))); + _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username))); return NotFound(new CommonResponse(ErrorCodes.Http.User.Get.NotExist, _localizer["ErrorGetUserNotExist"])); } return Ok(user); @@ -96,11 +97,11 @@ namespace Timeline.Controllers var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); switch (result) { - case PutResult.Created: - _logger.LogInformation(Log.Format(_localizer["LogPutCreate"], ("Username", username))); + case PutResult.Create: + _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username))); return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory)); - case PutResult.Modified: - _logger.LogInformation(Log.Format(_localizer["LogPutModify"], ("Username", username))); + case PutResult.Modify: + _logger.LogInformation(Log.Format(LogPutModify, ("Username", username))); return Ok(CommonPutResponse.Modify(_localizerFactory)); default: throw new InvalidBranchException(); @@ -108,7 +109,7 @@ namespace Timeline.Controllers } catch (UsernameBadFormatException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogPutBadUsername"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogPutBadUsername, ("Username", username))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Put.BadUsername, _localizer["ErrorPutBadUsername"])); } } @@ -123,7 +124,7 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogPatchUserNotExist"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); return NotFound(new CommonResponse(ErrorCodes.Http.User.Patch.NotExist, _localizer["ErrorPatchUserNotExist"])); } } @@ -134,12 +135,12 @@ namespace Timeline.Controllers try { await _userService.DeleteUser(username); - _logger.LogInformation(Log.Format(_localizer["LogDeleteDelete"], ("Username", username))); + _logger.LogInformation(Log.Format(LogDeleteDelete, ("Username", username))); return Ok(CommonDeleteResponse.Delete(_localizerFactory)); } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogDeleteUserNotExist"], ("Username", username))); + _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); return Ok(CommonDeleteResponse.NotExist(_localizerFactory)); } } @@ -150,19 +151,19 @@ namespace Timeline.Controllers try { await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - _logger.LogInformation(Log.Format(_localizer["LogChangeUsernameSuccess"], + _logger.LogInformation(Log.Format(LogChangeUsernameSuccess, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameNotExist"], + _logger.LogInformation(e, Log.Format(LogChangeUsernameNotExist, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.NotExist, _localizer["ErrorChangeUsernameNotExist", request.OldUsername])); } - catch (UserAlreadyExistException e) + catch (UsernameConfictException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangeUsernameAlreadyExist"], + _logger.LogInformation(e, Log.Format(LogChangeUsernameAlreadyExist, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangeUsername.AlreadyExist, _localizer["ErrorChangeUsernameAlreadyExist"])); } @@ -175,12 +176,12 @@ namespace Timeline.Controllers try { await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); - _logger.LogInformation(Log.Format(_localizer["LogChangePasswordSuccess"], ("Username", User.Identity.Name))); + _logger.LogInformation(Log.Format(LogChangePasswordSuccess, ("Username", User.Identity.Name))); return Ok(); } catch (BadPasswordException e) { - _logger.LogInformation(e, Log.Format(_localizer["LogChangePasswordBadPassword"], + _logger.LogInformation(e, Log.Format(LogChangePasswordBadPassword, ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); return BadRequest(new CommonResponse(ErrorCodes.Http.User.Op.ChangePassword.BadOldPassword, _localizer["ErrorChangePasswordBadPassword"])); diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs deleted file mode 100644 index 5e1183c1..00000000 --- a/Timeline/Controllers/UserDetailController.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Timeline.Authenticate; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Services; - -namespace Timeline.Controllers -{ - [Route("users/{username}")] - [ProducesErrorResponseType(typeof(CommonResponse))] - [ApiController] - public class UserDetailController : Controller - { - public static class ErrorCodes - { - public const int Get_UserNotExist = -1001; - - public const int Patch_Forbid = -2001; - public const int Patch_UserNotExist = -2002; - - public const int GetNickname_UserNotExist = -3001; - } - - private readonly ILogger _logger; - private readonly IUserDetailService _service; - - public UserDetailController(ILogger logger, IUserDetailService service) - { - _logger = logger; - _service = service; - } - - [HttpGet("nickname")] - [UserAuthorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetNickname([FromRoute] string username) - { - try - { - var nickname = await _service.GetUserNickname(username); - return Ok(new UserDetail - { - Nickname = nickname - }); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.GetNickname_UserNotExist, "The user does not exist.")); - } - } - - [HttpGet("details")] - [UserAuthorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Get([FromRoute] string username) - { - try - { - var detail = await _service.GetUserDetail(username); - return Ok(detail); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "The user does not exist.")); - } - } - - [HttpPatch("details")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Patch([FromRoute] string username, [FromBody] UserDetail detail) - { - if (!User.IsAdmin() && User.Identity.Name != username) - return StatusCode(StatusCodes.Status403Forbidden, new CommonResponse(ErrorCodes.Patch_Forbid, "You can't change other's details unless you are admin.")); - - try - { - await _service.UpdateUserDetail(username, detail); - return Ok(); - } - catch (UserNotExistException) - { - return NotFound(new CommonResponse(ErrorCodes.Patch_UserNotExist, "The user does not exist.")); - } - } - } -} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 130439d3..c741837a 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -86,13 +86,13 @@ namespace Timeline.Models.Http internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonPutResponse(0, localizer["ResponsePutCreate"], true); } internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonPutResponse(0, localizer["ResponsePutModify"], false); } @@ -123,13 +123,13 @@ namespace Timeline.Models.Http internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Http.Common"); return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true); } internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory) { - var localizer = localizerFactory.Create("Http.Common"); + var localizer = localizerFactory.Create("Models.Models.Http.Common"); return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false); } } diff --git a/Timeline/Models/PutResult.cs b/Timeline/Models/PutResult.cs index 544602eb..cecf86e6 100644 --- a/Timeline/Models/PutResult.cs +++ b/Timeline/Models/PutResult.cs @@ -8,10 +8,10 @@ namespace Timeline.Models /// /// Indicates the item did not exist and now is created. /// - Created, + Create, /// /// Indicates the item exists already and is modified. /// - Modified + Modify } } diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs deleted file mode 100644 index 302e3bb1..00000000 --- a/Timeline/Models/UserDetail.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Timeline.Entities; -using Timeline.Models.Validation; -using Newtonsoft.Json; - -namespace Timeline.Models -{ - public class UserDetail - { - [MaxLength(10)] - public string? Nickname { get; set; } - - [ValidateWith(typeof(UserDetailValidators.QQValidator))] - [JsonProperty(PropertyName = "qq")] - public string? QQ { get; set; } - - [ValidateWith(typeof(UserDetailValidators.EMailValidator))] - public string? Email { get; set; } - - [ValidateWith(typeof(UserDetailValidators.PhoneNumberValidator))] - public string? PhoneNumber { get; set; } - - public string? Description { get; set; } - - private static string? CoerceEmptyToNull(string? value) - { - if (string.IsNullOrEmpty(value)) - return null; - else - return value; - } - - public static UserDetail From(UserDetailEntity entity) - { - return new UserDetail - { - Nickname = CoerceEmptyToNull(entity.Nickname), - QQ = CoerceEmptyToNull(entity.QQ), - Email = CoerceEmptyToNull(entity.Email), - PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber), - Description = CoerceEmptyToNull(entity.Description) - }; - } - } -} diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs deleted file mode 100644 index 19c82edb..00000000 --- a/Timeline/Models/Validation/UserDetailValidator.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Net.Mail; - -namespace Timeline.Models.Validation -{ - public abstract class OptionalStringValidator : IValidator - { - public bool Validate(object value, out string message) - { - if (value == null) - { - message = ValidationConstants.SuccessMessage; - return true; - } - - if (value is string s) - { - if (s.Length == 0) - { - message = ValidationConstants.SuccessMessage; - return true; - } - return DoValidate(s, out message); - } - else - { - message = "Value is not of type string."; - return false; - } - } - - protected abstract bool DoValidate(string value, out string message); - } - - public static class UserDetailValidators - { - - public class QQValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length < 5) - { - message = "QQ is too short."; - return false; - } - - if (value.Length > 11) - { - message = "QQ is too long."; - return false; - } - - foreach (var c in value) - { - if (!char.IsDigit(c)) - { - message = "QQ must only contain digit."; - return false; - } - } - - message = ValidationConstants.SuccessMessage; - return true; - } - } - - public class EMailValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length > 50) - { - message = "E-Mail is too long."; - return false; - } - - try - { - var _ = new MailAddress(value); - } - catch (FormatException) - { - message = "The format of E-Mail is bad."; - return false; - } - message = ValidationConstants.SuccessMessage; - return true; - } - } - - public class PhoneNumberValidator : OptionalStringValidator - { - protected override bool DoValidate(string value, out string message) - { - if (value.Length > 14) - { - message = "Phone number is too long."; - return false; - } - - foreach (var c in value) - { - if (!char.IsDigit(c)) - { - message = "Phone number can only contain digit."; - return false; - } - } - - message = ValidationConstants.SuccessMessage; - return true; - } - } - } -} diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index e4891400..ecc3b5b3 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -1,4 +1,5 @@ -using System.Linq; +using Microsoft.Extensions.Localization; +using System.Linq; using System.Text.RegularExpressions; namespace Timeline.Models.Validation @@ -10,7 +11,7 @@ namespace Timeline.Models.Validation private readonly Regex _regex = new Regex(RegexPattern); - protected override bool DoValidate(string value, out string message) + protected override bool DoValidate(string value, IStringLocalizerFactory localizerFactory, out string message) { if (value.Length == 0) { diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a1acbed9..a3800b71 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -1,11 +1,14 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using System; using System.ComponentModel.DataAnnotations; +using Timeline.Helpers; namespace Timeline.Models.Validation { /// /// A validator to validate value. - /// See . + /// See . /// public interface IValidator { @@ -15,7 +18,7 @@ namespace Timeline.Models.Validation /// The value to validate. /// The validation message. /// True if validation passed. Otherwise false. - bool Validate(object value, out string message); + bool Validate(object? value, IStringLocalizerFactory localizerFactory, out string message); } public static class ValidationConstants @@ -36,27 +39,36 @@ namespace Timeline.Models.Validation /// public abstract class Validator : IValidator { - public bool Validate(object value, out string message) + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "")] + public bool Validate(object? value, IStringLocalizerFactory localizerFactory, out string message) { if (value == null) { - message = "Value is null."; + var localizer = localizerFactory.Create("Models.Validation.Validator"); + message = localizer["ValidatorMessageNull"]; return false; } if (value is T v) { - - return DoValidate(v, out message); + return DoValidate(v, localizerFactory, out message); } else { - message = $"Value is not of type {typeof(T).Name}"; + var localizer = localizerFactory.Create("Models.Validation.Validator"); + message = localizer["ValidatorMessageBadType", typeof(T).FullName]; return false; } } - protected abstract bool DoValidate(T value, out string message); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + protected static string GetSuccessMessage(IStringLocalizerFactory factory) + { + var localizer = factory.Create("Models.Validation.Validator"); + return localizer["ValidatorMessageSuccess"]; + } + + protected abstract bool DoValidate(T value, IStringLocalizerFactory localizerFactory, out string message); } [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, @@ -84,24 +96,28 @@ namespace Timeline.Models.Validation throw new ArgumentNullException(nameof(validatorType)); if (!typeof(IValidator).IsAssignableFrom(validatorType)) - throw new ArgumentException("Given type is not assignable to IValidator.", nameof(validatorType)); + throw new ArgumentException( + Resources.Models.Validation.Validator.ValidateWithAttributeNotValidator, + nameof(validatorType)); try { - _validator = Activator.CreateInstance(validatorType) as IValidator; + _validator = (Activator.CreateInstance(validatorType) as IValidator)!; } catch (Exception e) { - throw new ArgumentException("Failed to create a validator instance from default constructor. See inner exception.", e); + throw new ArgumentException( + Resources.Models.Validation.Validator.ValidateWithAttributeCreateFail, e); } } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - if (_validator.Validate(value, out var message)) + var localizerFactory = validationContext.GetRequiredService(); + if (_validator.Validate(value, localizerFactory, out var message)) return ValidationResult.Success; else - return new ValidationResult(string.Format("Field {0} is bad. {1}", validationContext.DisplayName, message)); + return new ValidationResult(message); } } } diff --git a/Timeline/Resources/Controllers/TokenController.Designer.cs b/Timeline/Resources/Controllers/TokenController.Designer.cs new file mode 100644 index 00000000..a7c2864b --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TokenController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TokenController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.TokenController", typeof(TokenController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The password is wrong.. + /// + internal static string LogBadPassword { + get { + return ResourceManager.GetString("LogBadPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user failed to create a token.. + /// + internal static string LogCreateFailure { + get { + return ResourceManager.GetString("LogCreateFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user succeeded to create a token.. + /// + internal static string LogCreateSuccess { + get { + return ResourceManager.GetString("LogCreateSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string LogUserNotExist { + get { + return ResourceManager.GetString("LogUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. + /// + internal static string LogVerifyBadFormat { + get { + return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired.. + /// + internal static string LogVerifyExpire { + get { + return ResourceManager.GetString("LogVerifyExpire", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token failed to be verified.. + /// + internal static string LogVerifyFailure { + get { + return ResourceManager.GetString("LogVerifyFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token has an old version. User might have update some info.. + /// + internal static string LogVerifyOldVersion { + get { + return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A token succeeded to be verified.. + /// + internal static string LogVerifySuccess { + get { + return ResourceManager.GetString("LogVerifySuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. + /// + internal static string LogVerifyUserNotExist { + get { + return ResourceManager.GetString("LogVerifyUserNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/UserController.Designer.cs b/Timeline/Resources/Controllers/UserController.Designer.cs new file mode 100644 index 00000000..df9cab4c --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserController", typeof(UserController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Attempt to change password with wrong old password failed.. + /// + internal static string LogChangePasswordBadPassword { + get { + return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user has changed password.. + /// + internal static string LogChangePasswordSuccess { + get { + return ResourceManager.GetString("LogChangePasswordSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change a user's username to a existent one failed.. + /// + internal static string LogChangeUsernameAlreadyExist { + get { + return ResourceManager.GetString("LogChangeUsernameAlreadyExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed.. + /// + internal static string LogChangeUsernameNotExist { + get { + return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user has changed username.. + /// + internal static string LogChangeUsernameSuccess { + get { + return ResourceManager.GetString("LogChangeUsernameSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user has been deleted.. + /// + internal static string LogDeleteDelete { + get { + return ResourceManager.GetString("LogDeleteDelete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a user that does not exist.. + /// + internal static string LogDeleteNotExist { + get { + return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to patch a user that does not exist failed.. + /// + internal static string LogPatchUserNotExist { + get { + return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to create a user with bad username failed.. + /// + internal static string LogPutBadUsername { + get { + return ResourceManager.GetString("LogPutBadUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user has been created.. + /// + internal static string LogPutCreate { + get { + return ResourceManager.GetString("LogPutCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user has been modified.. + /// + internal static string LogPutModify { + get { + return ResourceManager.GetString("LogPutModify", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Http/Common.en.resx b/Timeline/Resources/Http/Common.en.resx deleted file mode 100644 index 40d44191..00000000 --- a/Timeline/Resources/Http/Common.en.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - An existent item is deleted. - - - The item does not exist, so nothing is changed. - - - A new item is created. - - - An existent item is modified. - - \ No newline at end of file diff --git a/Timeline/Resources/Http/Common.zh.resx b/Timeline/Resources/Http/Common.zh.resx deleted file mode 100644 index b6d955d9..00000000 --- a/Timeline/Resources/Http/Common.zh.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 删除了一个项目。 - - - 要删除的项目不存在,什么都没有修改。 - - - 创建了一个新项目。 - - - 修改了一个已存在的项目。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx new file mode 100644 index 00000000..40d44191 --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.en.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An existent item is deleted. + + + The item does not exist, so nothing is changed. + + + A new item is created. + + + An existent item is modified. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx new file mode 100644 index 00000000..b6d955d9 --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 删除了一个项目。 + + + 要删除的项目不存在,什么都没有修改。 + + + 创建了一个新项目。 + + + 修改了一个已存在的项目。 + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.Designer.cs b/Timeline/Resources/Models/Validation/Validator.Designer.cs new file mode 100644 index 00000000..f2532af3 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Validator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Validator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.Validator", typeof(Validator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Failed to create a validator instance from default constructor. See inner exception.. + /// + internal static string ValidateWithAttributeCreateFail { + get { + return ResourceManager.GetString("ValidateWithAttributeCreateFail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Given type is not assignable to IValidator.. + /// + internal static string ValidateWithAttributeNotValidator { + get { + return ResourceManager.GetString("ValidateWithAttributeNotValidator", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/Validator.en.resx b/Timeline/Resources/Models/Validation/Validator.en.resx new file mode 100644 index 00000000..8d2fbede --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.en.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value is not of type {0}. + + + Value can't be null. + + + Validation succeeded. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.resx b/Timeline/Resources/Models/Validation/Validator.resx new file mode 100644 index 00000000..8843dc42 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to create a validator instance from default constructor. See inner exception. + + + Given type is not assignable to IValidator. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.zh.resx b/Timeline/Resources/Models/Validation/Validator.zh.resx new file mode 100644 index 00000000..2f98e7e3 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.zh.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 值不是类型{0}的实例。 + + + 值不能为null. + + + 验证成功。 + + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs new file mode 100644 index 00000000..15a8169e --- /dev/null +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -0,0 +1,189 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Exception { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Exception() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.Exception", typeof(Exception).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The password is wrong.. + /// + internal static string BadPasswordException { + get { + return ResourceManager.GetString("BadPasswordException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The version of the jwt token is old.. + /// + internal static string JwtBadVersionException { + get { + return ResourceManager.GetString("JwtBadVersionException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token didn't pass verification because {0}, see inner exception for information.. + /// + internal static string JwtVerifyException { + get { + return ResourceManager.GetString("JwtVerifyException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to token is expired.. + /// + internal static string JwtVerifyExceptionExpired { + get { + return ResourceManager.GetString("JwtVerifyExceptionExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id claim is not a number.. + /// + internal static string JwtVerifyExceptionIdClaimBadFormat { + get { + return ResourceManager.GetString("JwtVerifyExceptionIdClaimBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to id claim does not exist.. + /// + internal static string JwtVerifyExceptionNoIdClaim { + get { + return ResourceManager.GetString("JwtVerifyExceptionNoIdClaim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version claim does not exist.. + /// + internal static string JwtVerifyExceptionNoVersionClaim { + get { + return ResourceManager.GetString("JwtVerifyExceptionNoVersionClaim", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version of token is old.. + /// + internal static string JwtVerifyExceptionOldVersion { + get { + return ResourceManager.GetString("JwtVerifyExceptionOldVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to uncommon error.. + /// + internal static string JwtVerifyExceptionOthers { + get { + return ResourceManager.GetString("JwtVerifyExceptionOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error code.. + /// + internal static string JwtVerifyExceptionUnknown { + get { + return ResourceManager.GetString("JwtVerifyExceptionUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to version claim is not a number.. + /// + internal static string JwtVerifyExceptionVersionClaimBadFormat { + get { + return ResourceManager.GetString("JwtVerifyExceptionVersionClaimBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The username is of bad format.. + /// + internal static string UsernameBadFormatException { + get { + return ResourceManager.GetString("UsernameBadFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The username already exists.. + /// + internal static string UsernameConfictException { + get { + return ResourceManager.GetString("UsernameConfictException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string UserNotExistException { + get { + return ResourceManager.GetString("UserNotExistException", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx new file mode 100644 index 00000000..af771393 --- /dev/null +++ b/Timeline/Resources/Services/Exception.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The password is wrong. + + + The version of the jwt token is old. + + + The token didn't pass verification because {0}, see inner exception for information. + + + token is expired. + + + id claim is not a number. + + + id claim does not exist. + + + version claim does not exist. + + + version of token is old. + + + uncommon error. + + + unknown error code. + + + version claim is not a number. + + + The username is of bad format. + + + The username already exists. + + + The user does not exist. + + \ No newline at end of file diff --git a/Timeline/Services/BadPasswordException.cs b/Timeline/Services/BadPasswordException.cs new file mode 100644 index 00000000..ee8a42db --- /dev/null +++ b/Timeline/Services/BadPasswordException.cs @@ -0,0 +1,27 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + [Serializable] + public class BadPasswordException : Exception + { + public BadPasswordException() : base(Resources.Services.Exception.UserNotExistException) { } + public BadPasswordException(string message, Exception inner) : base(message, inner) { } + + public BadPasswordException(string badPassword) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Bad Password", badPassword))) + { + Password = badPassword; + } + + protected BadPasswordException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The wrong password. + /// + public string? Password { get; set; } + } +} diff --git a/Timeline/Services/JwtBadVersionException.cs b/Timeline/Services/JwtBadVersionException.cs new file mode 100644 index 00000000..4ce17710 --- /dev/null +++ b/Timeline/Services/JwtBadVersionException.cs @@ -0,0 +1,36 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + [Serializable] + public class JwtBadVersionException : Exception + { + public JwtBadVersionException() : base(Resources.Services.Exception.JwtBadVersionException) { } + public JwtBadVersionException(string message) : base(message) { } + public JwtBadVersionException(string message, Exception inner) : base(message, inner) { } + + public JwtBadVersionException(long tokenVersion, long requiredVersion) + : base(Log.Format(Resources.Services.Exception.JwtBadVersionException, + ("Token Version", tokenVersion), + ("Required Version", requiredVersion))) + { + TokenVersion = tokenVersion; + RequiredVersion = requiredVersion; + } + + protected JwtBadVersionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The version in the token. + /// + public long? TokenVersion { get; set; } + + /// + /// The version required. + /// + public long? RequiredVersion { get; set; } + } +} diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index 90d0c217..bf92966a 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System; +using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; @@ -14,63 +15,6 @@ namespace Timeline.Services public long Version { get; set; } } - [Serializable] - public class JwtTokenVerifyException : Exception - { - public static class ErrorCodes - { - // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server. - - public const int Others = -1001; - public const int NoIdClaim = -1002; - public const int IdClaimBadFormat = -1003; - public const int NoVersionClaim = -1004; - public const int VersionClaimBadFormat = -1005; - - /// - /// Corresponds to . - /// - public const int Expired = -2001; - } - - private const string message = "Jwt token is bad."; - - public JwtTokenVerifyException() : base(message) { } - public JwtTokenVerifyException(string message) : base(message) { } - public JwtTokenVerifyException(string message, Exception inner) : base(message, inner) { } - - public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; } - public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; } - public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; } - public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; } - protected JwtTokenVerifyException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public int ErrorCode { get; set; } - - private static string GetErrorMessage(int errorCode) - { - switch (errorCode) - { - case ErrorCodes.Others: - return "Uncommon error, see inner exception for more information."; - case ErrorCodes.NoIdClaim: - return "Id claim does not exist."; - case ErrorCodes.IdClaimBadFormat: - return "Id claim is not a number."; - case ErrorCodes.NoVersionClaim: - return "Version claim does not exist."; - case ErrorCodes.VersionClaimBadFormat: - return "Version claim is not a number"; - case ErrorCodes.Expired: - return "Token is expired."; - default: - return "Unknown error code."; - } - } - } - public interface IJwtService { /// @@ -89,7 +33,7 @@ namespace Timeline.Services /// The token string to verify. /// Return the saved info in token. /// Thrown when is null. - /// Thrown when the token is invalid. + /// Thrown when the token is invalid. TokenInfo VerifyJwtToken(string token); } @@ -116,8 +60,8 @@ namespace Timeline.Services var config = _jwtConfig.CurrentValue; var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64)); var tokenDescriptor = new SecurityTokenDescriptor() { @@ -159,15 +103,15 @@ namespace Timeline.Services var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); if (idClaim == null) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim); if (!long.TryParse(idClaim, out var id)) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat); var versionClaim = principal.FindFirstValue(VersionClaimType); if (versionClaim == null) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim); if (!long.TryParse(versionClaim, out var version)) - throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat); + throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat); return new TokenInfo { @@ -177,11 +121,11 @@ namespace Timeline.Services } catch (SecurityTokenExpiredException e) { - throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired); + throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired); } catch (Exception e) { - throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others); + throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others); } } } diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs new file mode 100644 index 00000000..a915b51a --- /dev/null +++ b/Timeline/Services/JwtVerifyException.cs @@ -0,0 +1,59 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Globalization; +using static Timeline.Resources.Services.Exception; + +namespace Timeline.Services +{ + [Serializable] + public class JwtVerifyException : Exception + { + public static class ErrorCodes + { + // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server. + + public const int Others = -1001; + public const int NoIdClaim = -1002; + public const int IdClaimBadFormat = -1003; + public const int NoVersionClaim = -1004; + public const int VersionClaimBadFormat = -1005; + + /// + /// Corresponds to . + /// + public const int Expired = -2001; + public const int OldVersion = -2002; + } + + public JwtVerifyException() : base(GetErrorMessage(0)) { } + public JwtVerifyException(string message) : base(message) { } + public JwtVerifyException(string message, Exception inner) : base(message, inner) { } + + public JwtVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; } + public JwtVerifyException(string message, int code) : base(message) { ErrorCode = code; } + public JwtVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; } + public JwtVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; } + protected JwtVerifyException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public int ErrorCode { get; set; } + + private static string GetErrorMessage(int errorCode) + { + var reason = errorCode switch + { + ErrorCodes.Others => JwtVerifyExceptionOthers, + ErrorCodes.NoIdClaim => JwtVerifyExceptionNoIdClaim, + ErrorCodes.IdClaimBadFormat => JwtVerifyExceptionIdClaimBadFormat, + ErrorCodes.NoVersionClaim => JwtVerifyExceptionNoVersionClaim, + ErrorCodes.VersionClaimBadFormat => JwtVerifyExceptionVersionClaimBadFormat, + ErrorCodes.Expired => JwtVerifyExceptionExpired, + ErrorCodes.OldVersion => JwtVerifyExceptionOldVersion, + _ => JwtVerifyExceptionUnknown + }; + + return string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.JwtVerifyException, reason); + } + } +} diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs deleted file mode 100644 index 5e049435..00000000 --- a/Timeline/Services/UserDetailService.cs +++ /dev/null @@ -1,135 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models; - -namespace Timeline.Services -{ - public interface IUserDetailService - { - /// - /// Get the nickname of user. - /// - /// The username to get nickname of. - /// The user's nickname. Null if not set. - /// Thrown if is null or empty. - /// Thrown if user doesn't exist. - Task GetUserNickname(string username); - - /// - /// Get the detail of user. - /// - /// The username to get user detail of. - /// The user detail. - /// Thrown if is null or empty. - /// Thrown if user doesn't exist. - Task GetUserDetail(string username); - - /// - /// Update the detail of user. This function does not do data check. - /// - /// The username to get user detail of. - /// The detail to update. Can't be null. Any null member means not set. - /// Thrown if is null or empty or is null. - /// Thrown if user doesn't exist. - Task UpdateUserDetail(string username, UserDetail detail); - } - - public class UserDetailService : IUserDetailService - { - private readonly ILogger _logger; - - private readonly DatabaseContext _databaseContext; - - public UserDetailService(ILogger logger, DatabaseContext databaseContext) - { - _logger = logger; - _databaseContext = databaseContext; - } - - private async Task CreateEntity(long userId) - { - var entity = new UserDetailEntity() - { - UserId = userId - }; - _databaseContext.UserDetails.Add(entity); - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation("An entity is created in user_details."); - return entity; - } - - // Check the existence of user detail entry - private async Task CheckAndInit(long userId) - { - var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync(); - if (detail == null) - { - detail = await CreateEntity(userId); - } - return detail; - } - - public async Task GetUserNickname(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).Select(e => new { e.Nickname }).SingleOrDefaultAsync(); - if (detail == null) - { - var entity = await CreateEntity(userId); - return null; - } - else - { - var nickname = detail.Nickname; - return string.IsNullOrEmpty(nickname) ? null : nickname; - } - } - - public async Task GetUserDetail(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detailEntity = await CheckAndInit(userId); - return UserDetail.From(detailEntity); - } - - public async Task UpdateUserDetail(string username, UserDetail detail) - { - if (detail == null) - throw new ArgumentNullException(nameof(detail)); - - var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); - var detailEntity = await CheckAndInit(userId); - - if (detail.Nickname != null) - detailEntity.Nickname = detail.Nickname; - - if (detail.QQ != null) - detailEntity.QQ = detail.QQ; - - if (detail.Email != null) - detailEntity.Email = detail.Email; - - if (detail.PhoneNumber != null) - detailEntity.PhoneNumber = detail.PhoneNumber; - - if (detail.Description != null) - detailEntity.Description = detail.Description; - - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation("An entity is updated in user_details."); - } - } - - public static class UserDetailServiceCollectionExtensions - { - public static void AddUserDetailService(this IServiceCollection services) - { - services.AddScoped(); - } - } -} diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs new file mode 100644 index 00000000..c7317f56 --- /dev/null +++ b/Timeline/Services/UserNotExistException.cs @@ -0,0 +1,41 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + /// + /// The user requested does not exist. + /// + [Serializable] + public class UserNotExistException : Exception + { + public UserNotExistException() : base(Resources.Services.Exception.UserNotExistException) { } + public UserNotExistException(string message, Exception inner) : base(message, inner) { } + + public UserNotExistException(string username) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Username", username))) + { + Username = username; + } + + public UserNotExistException(long id) + : base(Log.Format(Resources.Services.Exception.UserNotExistException, ("Id", id))) + { + Id = id; + } + + protected UserNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The username of the user that does not exist. + /// + public string? Username { get; set; } + + /// + /// The id of the user that does not exist. + /// + public long? Id { get; set; } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 9564b34b..aad4a806 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Helpers.MyLogHelper; @@ -15,163 +15,8 @@ namespace Timeline.Services { public class CreateTokenResult { - public string Token { get; set; } - public UserInfo User { get; set; } - } - - [Serializable] - public class UserNotExistException : Exception - { - private const string message = "The user does not exist."; - - public UserNotExistException() - : base(message) - { - - } - - public UserNotExistException(string username) - : base(Log.Format(message, ("Username", username))) - { - Username = username; - } - - public UserNotExistException(long id) - : base(Log.Format(message, ("Id", id))) - { - Id = id; - } - - public UserNotExistException(string message, Exception inner) : base(message, inner) { } - - protected UserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that does not exist. - /// - public string Username { get; set; } - - /// - /// The id that does not exist. - /// - public long? Id { get; set; } - } - - [Serializable] - public class BadPasswordException : Exception - { - private const string message = "Password is wrong."; - - public BadPasswordException() - : base(message) - { - - } - - public BadPasswordException(string badPassword) - : base(Log.Format(message, ("Bad Password", badPassword))) - { - Password = badPassword; - } - - public BadPasswordException(string message, Exception inner) : base(message, inner) { } - - protected BadPasswordException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The wrong password. - /// - public string Password { get; set; } - } - - - [Serializable] - public class BadTokenVersionException : Exception - { - private const string message = "Token version is expired."; - - public BadTokenVersionException() - : base(message) - { - - } - - public BadTokenVersionException(long tokenVersion, long requiredVersion) - : base(Log.Format(message, - ("Token Version", tokenVersion), - ("Required Version", requiredVersion))) - { - TokenVersion = tokenVersion; - RequiredVersion = requiredVersion; - } - - public BadTokenVersionException(string message) : base(message) { } - public BadTokenVersionException(string message, Exception inner) : base(message, inner) { } - - protected BadTokenVersionException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The version in the token. - /// - public long? TokenVersion { get; set; } - - /// - /// The version required. - /// - public long? RequiredVersion { get; set; } - } - - /// - /// Thrown when username is of bad format. - /// - [Serializable] - public class UsernameBadFormatException : Exception - { - private const string message = "Username is of bad format."; - - public UsernameBadFormatException() : base(message) { } - public UsernameBadFormatException(string message) : base(message) { } - public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } - - public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } - public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } - protected UsernameBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// Username of bad format. - /// - public string Username { get; private set; } - } - - - /// - /// Thrown when the user already exists. - /// - [Serializable] - public class UserAlreadyExistException : Exception - { - private const string message = "User already exists."; - - public UserAlreadyExistException() : base(message) { } - public UserAlreadyExistException(string username) : base(Log.Format(message, ("Username", username))) { Username = username; } - public UserAlreadyExistException(string username, string message) : base(message) { Username = username; } - public UserAlreadyExistException(string message, Exception inner) : base(message, inner) { } - protected UserAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that already exists. - /// - public string Username { get; set; } + public string Token { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public interface IUserService @@ -196,9 +41,8 @@ namespace Timeline.Services /// The token to verify. /// The user info specified by the token. /// Thrown when is null. - /// Thrown when the token is of bad format. Thrown by . + /// Thrown when the token is of bad format. Thrown by . /// Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued. - /// Thrown when the version in the token is expired. User needs to recreate the token. Task VerifyToken(string token); /// @@ -221,10 +65,12 @@ namespace Timeline.Services /// Username of user. /// Password of user. /// Whether the user is administrator. - /// Return if a new user is created. - /// Return if a existing user is modified. - /// Thrown when is of bad format. + /// + /// Return if a new user is created. + /// Return if a existing user is modified. + /// /// Thrown when or is null. + /// Thrown when is of bad format. Task PutUser(string username, string password, bool administrator); /// @@ -237,7 +83,7 @@ namespace Timeline.Services /// Whether the user is administrator. Null if not modify. /// Thrown if is null. /// Thrown if the user with given username does not exist. - Task PatchUser(string username, string password, bool? administrator); + Task PatchUser(string username, string? password, bool? administrator); /// /// Delete a user of given username. @@ -266,13 +112,13 @@ namespace Timeline.Services /// Thrown if or is null or empty. /// Thrown if the user with old username does not exist. /// Thrown if the new username is not accepted because of bad format. - /// Thrown if user with the new username already exists. + /// Thrown if user with the new username already exists. Task ChangeUsername(string oldUsername, string newUsername); } internal class UserCache { - public string Username { get; set; } + public string Username { get; set; } = default!; public bool Administrator { get; set; } public long Version { get; set; } @@ -294,13 +140,16 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator; - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + private readonly IStringLocalizerFactory _localizerFactory; + + public UserService(ILogger logger, IMemoryCache memoryCache, IStringLocalizerFactory localizerFactory, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) { _logger = logger; _memoryCache = memoryCache; _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; + _localizerFactory = localizerFactory; _usernameValidator = new UsernameValidator(); } @@ -368,7 +217,7 @@ namespace Timeline.Services } if (tokenInfo.Version != cache.Version) - throw new BadTokenVersionException(tokenInfo.Version, cache.Version); + throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion); return cache.ToUserInfo(); } @@ -395,7 +244,7 @@ namespace Timeline.Services if (password == null) throw new ArgumentNullException(nameof(password)); - if (!_usernameValidator.Validate(username, out var message)) + if (!_usernameValidator.Validate(username, _localizerFactory, out var message)) { throw new UsernameBadFormatException(username, message); } @@ -414,7 +263,7 @@ namespace Timeline.Services await _databaseContext.AddAsync(newUser); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(FormatLogMessage("A new user entry is added to the database.", Pair("Id", newUser.Id))); - return PutResult.Created; + return PutResult.Create; } user.EncryptedPassword = _passwordService.HashPassword(password); @@ -426,7 +275,7 @@ namespace Timeline.Services //clear cache RemoveCache(user.Id); - return PutResult.Modified; + return PutResult.Modify; } public async Task PatchUser(string username, string password, bool? administrator) @@ -504,7 +353,7 @@ namespace Timeline.Services if (string.IsNullOrEmpty(newUsername)) throw new ArgumentException("New username is null or empty", nameof(newUsername)); - if (!_usernameValidator.Validate(newUsername, out var message)) + if (!_usernameValidator.Validate(newUsername, _localizerFactory, out var message)) throw new UsernameBadFormatException(newUsername, $"New username is of bad format. {message}"); var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync(); @@ -513,7 +362,7 @@ namespace Timeline.Services var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync(); if (conflictUser != null) - throw new UserAlreadyExistException(newUsername); + throw new UsernameConfictException(newUsername); user.Name = newUsername; user.Version += 1; diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs new file mode 100644 index 00000000..04354d22 --- /dev/null +++ b/Timeline/Services/UsernameBadFormatException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Thrown when username is of bad format. + /// + [Serializable] + public class UsernameBadFormatException : Exception + { + public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { } + public UsernameBadFormatException(string message) : base(message) { } + public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } + + public UsernameBadFormatException(string username, string message) : base(message) { Username = username; } + public UsernameBadFormatException(string username, string message, Exception inner) : base(message, inner) { Username = username; } + + protected UsernameBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// Username of bad format. + /// + public string? Username { get; private set; } + } +} diff --git a/Timeline/Services/UsernameConfictException.cs b/Timeline/Services/UsernameConfictException.cs new file mode 100644 index 00000000..fde1eda6 --- /dev/null +++ b/Timeline/Services/UsernameConfictException.cs @@ -0,0 +1,25 @@ +using System; +using Timeline.Helpers; + +namespace Timeline.Services +{ + /// + /// Thrown when the user already exists. + /// + [Serializable] + public class UsernameConfictException : Exception + { + public UsernameConfictException() : base(Resources.Services.Exception.UsernameConfictException) { } + public UsernameConfictException(string username) : base(Log.Format(Resources.Services.Exception.UsernameConfictException, ("Username", username))) { Username = username; } + public UsernameConfictException(string username, string message) : base(message) { Username = username; } + public UsernameConfictException(string message, Exception inner) : base(message, inner) { } + protected UsernameConfictException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The username that already exists. + /// + public string? Username { get; set; } + } +} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 5718cf05..be5bce7c 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -64,7 +64,6 @@ namespace Timeline services.AddTransient(); services.AddUserAvatarService(); - services.AddUserDetailService(); var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index a948e3f3..d1f9b2ed 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -39,6 +39,26 @@ True Common.resx + + True + True + TokenController.resx + + + True + True + UserController.resx + + + True + True + Validator.resx + + + True + True + Exception.resx + @@ -48,7 +68,8 @@ Designer - + ResXFileCodeGenerator + TokenController.Designer.cs Designer @@ -58,7 +79,16 @@ - + ResXFileCodeGenerator + UserController.Designer.cs + + + ResXFileCodeGenerator + Validator.Designer.cs + + + ResXFileCodeGenerator + Exception.Designer.cs -- cgit v1.2.3 From 9a163719b76958374d1c27616393368e54e8b8a5 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 23 Oct 2019 20:41:19 +0800 Subject: ... --- Timeline.Tests/Controllers/UserControllerTest.cs | 17 -- Timeline.Tests/DatabaseTest.cs | 16 -- .../IntegratedTests/AuthorizationTest.cs | 69 +++++ .../IntegratedTests/AuthorizationUnitTest.cs | 69 ----- Timeline.Tests/IntegratedTests/TokenTest.cs | 176 +++++++++++++ Timeline.Tests/IntegratedTests/TokenUnitTest.cs | 176 ------------- Timeline.Tests/IntegratedTests/UserTest.cs | 277 +++++++++++++++++++++ Timeline.Tests/IntegratedTests/UserUnitTest.cs | 264 -------------------- Timeline.Tests/Mock/Data/TestUsers.cs | 2 +- Timeline/Authenticate/Attribute.cs | 21 -- Timeline/Authenticate/AuthHandler.cs | 102 -------- Timeline/Authenticate/PrincipalExtensions.cs | 13 - Timeline/Authentication/Attribute.cs | 21 ++ Timeline/Authentication/AuthHandler.cs | 98 ++++++++ Timeline/Authentication/PrincipalExtensions.cs | 13 + Timeline/Configs/DatabaseConfig.cs | 2 +- Timeline/Configs/JwtConfig.cs | 6 +- .../Controllers/Testing/TestingAuthController.cs | 2 +- Timeline/Controllers/UserAvatarController.cs | 6 +- Timeline/Controllers/UserController.cs | 44 ++-- Timeline/Entities/DatabaseContext.cs | 37 +-- Timeline/Entities/User.cs | 32 +++ Timeline/Entities/UserDetail.cs | 29 --- Timeline/GlobalSuppressions.cs | 1 + .../Helpers/StringLocalizerFactoryExtensions.cs | 5 + Timeline/Models/Http/User.cs | 4 +- Timeline/Models/UserConvert.cs | 67 +++++ Timeline/Models/UserInfo.cs | 4 +- Timeline/Models/UserUtility.cs | 60 ----- Timeline/Models/Validation/UsernameValidator.cs | 14 +- Timeline/Models/Validation/Validator.cs | 2 +- Timeline/Program.cs | 1 + .../Authentication/AuthHandler.Designer.cs | 99 ++++++++ Timeline/Resources/Authentication/AuthHandler.resx | 132 ++++++++++ Timeline/Resources/Services/Exception.Designer.cs | 63 +++++ Timeline/Resources/Services/Exception.resx | 21 ++ .../Resources/Services/UserService.Designer.cs | 126 ++++++++++ Timeline/Resources/Services/UserService.resx | 141 +++++++++++ Timeline/Services/PasswordService.cs | 38 +-- Timeline/Services/UserService.cs | 95 ++++--- Timeline/Startup.cs | 2 +- Timeline/Timeline.csproj | 18 ++ 42 files changed, 1488 insertions(+), 897 deletions(-) create mode 100644 Timeline.Tests/IntegratedTests/AuthorizationTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs create mode 100644 Timeline.Tests/IntegratedTests/TokenTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/TokenUnitTest.cs create mode 100644 Timeline.Tests/IntegratedTests/UserTest.cs delete mode 100644 Timeline.Tests/IntegratedTests/UserUnitTest.cs delete mode 100644 Timeline/Authenticate/Attribute.cs delete mode 100644 Timeline/Authenticate/AuthHandler.cs delete mode 100644 Timeline/Authenticate/PrincipalExtensions.cs create mode 100644 Timeline/Authentication/Attribute.cs create mode 100644 Timeline/Authentication/AuthHandler.cs create mode 100644 Timeline/Authentication/PrincipalExtensions.cs create mode 100644 Timeline/Entities/User.cs delete mode 100644 Timeline/Entities/UserDetail.cs create mode 100644 Timeline/Models/UserConvert.cs delete mode 100644 Timeline/Models/UserUtility.cs create mode 100644 Timeline/Resources/Authentication/AuthHandler.Designer.cs create mode 100644 Timeline/Resources/Authentication/AuthHandler.resx create mode 100644 Timeline/Resources/Services/UserService.Designer.cs create mode 100644 Timeline/Resources/Services/UserService.resx (limited to 'Timeline.Tests/Controllers/UserControllerTest.cs') diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 471ed851..781ec111 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -89,23 +89,6 @@ namespace Timeline.Tests.Controllers response.Data.Create.Should().Be(create); } - [Fact] - public async Task Put_BadUsername() - { - const string username = "aaa"; - const string password = "ppp"; - const bool administrator = true; - _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ThrowsAsync(new UsernameBadFormatException()); - var action = await _controller.Put(new UserPutRequest - { - Password = password, - Administrator = administrator - }, username); - action.Result.Should().BeAssignableTo() - .Which.Value.Should().BeAssignableTo() - .Which.Code.Should().Be(Put.BadUsername); - } - [Fact] public async Task Patch_Success() { diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index f75ab71b..c45c0f66 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -32,21 +32,5 @@ namespace Timeline.Tests _context.SaveChanges(); _context.UserAvatars.Count().Should().Be(1); } - - [Fact] - public void DeleteUserShouldAlsoDeleteDetail() - { - var user = _context.Users.First(); - _context.UserDetails.Add(new UserDetailEntity - { - UserId = user.Id - }); - _context.SaveChanges(); - _context.UserDetails.Count().Should().Be(1); - - _context.Users.Remove(user); - _context.SaveChanges(); - _context.UserDetails.Count().Should().Be(0); - } } } diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..a31d98f5 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class AuthorizationTest : IClassFixture>, IDisposable + { + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public AuthorizationTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + private const string BaseUrl = "testing/auth/"; + private const string AuthorizeUrl = BaseUrl + "Authorize"; + private const string UserUrl = BaseUrl + "User"; + private const string AdminUrl = BaseUrl + "Admin"; + + [Fact] + public async Task UnauthenticationTest() + { + using var client = _factory.CreateDefaultClient(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AuthenticationTest() + { + using var client = await _factory.CreateClientAsUser(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task UserAuthorizationTest() + { + using var client = await _factory.CreateClientAsUser(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using var client = await _factory.CreateClientAsAdmin(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs deleted file mode 100644 index 588e4349..00000000 --- a/Timeline.Tests/IntegratedTests/AuthorizationUnitTest.cs +++ /dev/null @@ -1,69 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Net; -using System.Threading.Tasks; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Xunit; - -namespace Timeline.Tests.IntegratedTests -{ - public class AuthorizationUnitTest : IClassFixture>, IDisposable - { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public AuthorizationUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - private const string BaseUrl = "testing/auth/"; - private const string AuthorizeUrl = BaseUrl + "Authorize"; - private const string UserUrl = BaseUrl + "User"; - private const string AdminUrl = BaseUrl + "Admin"; - - [Fact] - public async Task UnauthenticationTest() - { - using var client = _factory.CreateDefaultClient(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); - } - - [Fact] - public async Task AuthenticationTest() - { - using var client = await _factory.CreateClientAsUser(); - var response = await client.GetAsync(AuthorizeUrl); - response.Should().HaveStatusCode(HttpStatusCode.OK); - } - - [Fact] - public async Task UserAuthorizationTest() - { - using var client = await _factory.CreateClientAsUser(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); - } - - [Fact] - public async Task AdminAuthorizationTest() - { - using var client = await _factory.CreateClientAsAdmin(); - var response1 = await client.GetAsync(UserUrl); - response1.Should().HaveStatusCode(HttpStatusCode.OK); - var response2 = await client.GetAsync(AdminUrl); - response2.Should().HaveStatusCode(HttpStatusCode.OK); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..e9b6e1e9 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,176 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; +using Xunit; +using static Timeline.ErrorCodes.Http.Token; + +namespace Timeline.Tests.IntegratedTests +{ + public class TokenTest : IClassFixture>, IDisposable + { + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; + + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public TokenTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + public static IEnumerable CreateToken_InvalidModel_Data() + { + yield return new[] { null, "p", null }; + yield return new[] { "u", null, null }; + yield return new object[] { "u", "p", 2000 }; + yield return new object[] { "u", "p", -1 }; + } + + [Theory] + [MemberData(nameof(CreateToken_InvalidModel_Data))] + public async Task CreateToken_InvalidModel(string username, string password, int expire) + { + using var client = _factory.CreateDefaultClient(); + (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest + { + Username = username, + Password = password, + Expire = expire + })).Should().BeInvalidModel(); + } + + public static IEnumerable CreateToken_UserCredential_Data() + { + yield return new[] { "usernotexist", "p" }; + yield return new[] { MockUser.User.Username, "???" }; + } + + [Theory] + [MemberData(nameof(CreateToken_UserCredential_Data))] + public async void CreateToken_UserCredential(string username, string password) + { + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Create.BadCredential); + } + + [Fact] + public async Task CreateToken_Success() + { + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); + var body = response.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody().Which; + body.Token.Should().NotBeNullOrWhiteSpace(); + body.User.Should().BeEquivalentTo(MockUser.User.Info); + } + + [Fact] + public async Task VerifyToken_InvalidModel() + { + using var client = _factory.CreateDefaultClient(); + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); + } + + [Fact] + public async Task VerifyToken_BadFormat() + { + using var client = _factory.CreateDefaultClient(); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = "bad token hahaha" }); + response.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.BadFormat); + } + + [Fact] + public async Task VerifyToken_OldVersion() + { + using var client = _factory.CreateDefaultClient(); + var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. + { + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.PatchUser(MockUser.User.Username, null, null); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.OldVersion); + } + + [Fact] + public async Task VerifyToken_UserNotExist() + { + using var client = _factory.CreateDefaultClient(); + var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; + + using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. + { + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser(MockUser.User.Username); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Verify.UserNotExist); + } + + //[Fact] + //public async Task VerifyToken_Expired() + //{ + // using (var client = _factory.CreateDefaultClient()) + // { + // // I can only control the token expired time but not current time + // // because verify logic is encapsuled in other library. + // var mockClock = _factory.GetTestClock(); + // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); + // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; + // var response = await client.PostAsJsonAsync(VerifyTokenUrl, + // new VerifyTokenRequest { Token = token }); + // response.Should().HaveStatusCodeBadRequest() + // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); + // mockClock.MockCurrentTime = null; + // } + //} + + [Fact] + public async Task VerifyToken_Success() + { + using var client = _factory.CreateDefaultClient(); + var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = createTokenResult.Token }); + response.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.User.Should().BeEquivalentTo(MockUser.User.Info); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/TokenUnitTest.cs b/Timeline.Tests/IntegratedTests/TokenUnitTest.cs deleted file mode 100644 index d30b9311..00000000 --- a/Timeline.Tests/IntegratedTests/TokenUnitTest.cs +++ /dev/null @@ -1,176 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Models.Http; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; -using Xunit; -using static Timeline.ErrorCodes.Http.Token; - -namespace Timeline.Tests.IntegratedTests -{ - public class TokenUnitTest : IClassFixture>, IDisposable - { - private const string CreateTokenUrl = "token/create"; - private const string VerifyTokenUrl = "token/verify"; - - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public TokenUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - public static IEnumerable CreateToken_InvalidModel_Data() - { - yield return new[] { null, "p", null }; - yield return new[] { "u", null, null }; - yield return new object[] { "u", "p", 2000 }; - yield return new object[] { "u", "p", -1 }; - } - - [Theory] - [MemberData(nameof(CreateToken_InvalidModel_Data))] - public async Task CreateToken_InvalidModel(string username, string password, int expire) - { - using var client = _factory.CreateDefaultClient(); - (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest - { - Username = username, - Password = password, - Expire = expire - })).Should().BeInvalidModel(); - } - - public static IEnumerable CreateToken_UserCredential_Data() - { - yield return new[] { "usernotexist", "p" }; - yield return new[] { MockUser.User.Username, "???" }; - } - - [Theory] - [MemberData(nameof(CreateToken_UserCredential_Data))] - public async void CreateToken_UserCredential(string username, string password) - { - using var client = _factory.CreateDefaultClient(); - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = username, Password = password }); - response.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Create.BadCredential); - } - - [Fact] - public async Task CreateToken_Success() - { - using var client = _factory.CreateDefaultClient(); - var response = await client.PostAsJsonAsync(CreateTokenUrl, - new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); - var body = response.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody().Which; - body.Token.Should().NotBeNullOrWhiteSpace(); - body.User.Should().BeEquivalentTo(MockUser.User.Info); - } - - [Fact] - public async Task VerifyToken_InvalidModel() - { - using var client = _factory.CreateDefaultClient(); - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); - } - - [Fact] - public async Task VerifyToken_BadFormat() - { - using var client = _factory.CreateDefaultClient(); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = "bad token hahaha" }); - response.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Verify.BadFormat); - } - - [Fact] - public async Task VerifyToken_OldVersion() - { - using var client = _factory.CreateDefaultClient(); - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; - - using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. - { - // create a user for test - var userService = scope.ServiceProvider.GetRequiredService(); - await userService.PatchUser(MockUser.User.Username, null, null); - } - - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token })) - .Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Verify.OldVersion); - } - - [Fact] - public async Task VerifyToken_UserNotExist() - { - using var client = _factory.CreateDefaultClient(); - var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token; - - using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped. - { - var userService = scope.ServiceProvider.GetRequiredService(); - await userService.DeleteUser(MockUser.User.Username); - } - - (await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token })) - .Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Verify.UserNotExist); - } - - //[Fact] - //public async Task VerifyToken_Expired() - //{ - // using (var client = _factory.CreateDefaultClient()) - // { - // // I can only control the token expired time but not current time - // // because verify logic is encapsuled in other library. - // var mockClock = _factory.GetTestClock(); - // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); - // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; - // var response = await client.PostAsJsonAsync(VerifyTokenUrl, - // new VerifyTokenRequest { Token = token }); - // response.Should().HaveStatusCodeBadRequest() - // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); - // mockClock.MockCurrentTime = null; - // } - //} - - [Fact] - public async Task VerifyToken_Success() - { - using var client = _factory.CreateDefaultClient(); - var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password); - var response = await client.PostAsJsonAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); - response.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.User.Should().BeEquivalentTo(MockUser.User.Info); - } - } -} diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..ec70b7e8 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,277 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; +using Xunit; +using static Timeline.ErrorCodes.Http.User; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserTest : IClassFixture>, IDisposable + { + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public UserTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task Get_List_Success() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.Should().BeEquivalentTo(MockUser.UserInfoList); + } + + [Fact] + public async Task Get_Single_Success() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users/" + MockUser.User.Username); + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.Should().BeEquivalentTo(MockUser.User.Info); + } + + [Fact] + public async Task Get_InvalidModel() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Get_Users_404() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.GetAsync("users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Get.NotExist); + } + + public static IEnumerable Put_InvalidModel_Data() + { + yield return new object[] { "aaa", null, false }; + yield return new object[] { "aaa", "p", null }; + yield return new object[] { "aa!a", "p", false }; + } + + [Theory] + [MemberData(nameof(Put_InvalidModel_Data))] + public async Task Put_InvalidModel(string username, string password, bool? administrator) + { + using var client = await _factory.CreateClientAsAdmin(); + (await client.PutAsJsonAsync("users/" + username, + new UserPutRequest { Password = password, Administrator = administrator })) + .Should().BeInvalidModel(); + } + + private async Task CheckAdministrator(HttpClient client, string username, bool administrator) + { + var res = await client.GetAsync("users/" + username); + res.Should().HaveStatusCode(200) + .And.Should().HaveJsonBody() + .Which.Administrator.Should().Be(administrator); + } + + [Fact] + public async Task Put_Modiefied() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest + { + Password = "password", + Administrator = false + }); + res.Should().BePut(false); + await CheckAdministrator(client, MockUser.User.Username, false); + } + + [Fact] + public async Task Put_Created() + { + using var client = await _factory.CreateClientAsAdmin(); + const string username = "puttest"; + const string url = "users/" + username; + + var res = await client.PutAsJsonAsync(url, new UserPutRequest + { + Password = "password", + Administrator = false + }); + res.Should().BePut(true); + await CheckAdministrator(client, username, false); + } + + [Fact] + public async Task Patch_NotExist() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCode(404) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Patch.NotExist); + } + + [Fact] + public async Task Patch_InvalidModel() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Patch_Success() + { + using var client = await _factory.CreateClientAsAdmin(); + { + var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, + new UserPatchRequest { Administrator = false }); + res.Should().HaveStatusCode(200); + await CheckAdministrator(client, MockUser.User.Username, false); + } + } + + [Fact] + public async Task Delete_InvalidModel() + { + using var client = await _factory.CreateClientAsAdmin(); + var url = "users/aaa!a"; + var res = await client.DeleteAsync(url); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Delete_Deleted() + { + using var client = await _factory.CreateClientAsAdmin(); + var url = "users/" + MockUser.User.Username; + var res = await client.DeleteAsync(url); + res.Should().BeDelete(true); + + var res2 = await client.GetAsync(url); + res2.Should().HaveStatusCode(404); + } + + [Fact] + public async Task Delete_NotExist() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDelete(false); + } + + private const string changeUsernameUrl = "userop/changeusername"; + + public static IEnumerable Op_ChangeUsername_InvalidModel_Data() + { + yield return new[] { null, "uuu" }; + yield return new[] { "uuu", null }; + yield return new[] { "a!a", "uuu" }; + yield return new[] { "uuu", "a!a" }; + } + + [Theory] + [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))] + public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername) + { + using var client = await _factory.CreateClientAsAdmin(); + (await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) + .Should().BeInvalidModel(); + } + + [Fact] + public async Task Op_ChangeUsername_UserNotExist() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangeUsername.NotExist); + } + + [Fact] + public async Task Op_ChangeUsername_UserAlreadyExist() + { + using var client = await _factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); + } + + [Fact] + public async Task Op_ChangeUsername_Success() + { + using var client = await _factory.CreateClientAsAdmin(); + const string newUsername = "hahaha"; + var res = await client.PostAsJsonAsync(changeUsernameUrl, + new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); + res.Should().HaveStatusCode(200); + await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); + } + + private const string changePasswordUrl = "userop/changepassword"; + + public static IEnumerable Op_ChangePassword_InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } + + [Theory] + [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] + public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) + { + using var client = await _factory.CreateClientAsUser(); + (await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) + .Should().BeInvalidModel(); + } + + [Fact] + public async Task Op_ChangePassword_BadOldPassword() + { + using var client = await _factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(400) + .And.Should().HaveCommonBody() + .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); + } + + [Fact] + public async Task Op_ChangePassword_Success() + { + using var client = await _factory.CreateClientAsUser(); + const string newPassword = "new"; + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); + res.Should().HaveStatusCode(200); + await _factory.CreateDefaultClient() // don't use client above, because it sets authorization header + .CreateUserTokenAsync(MockUser.User.Username, newPassword); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/UserUnitTest.cs b/Timeline.Tests/IntegratedTests/UserUnitTest.cs deleted file mode 100644 index b00648de..00000000 --- a/Timeline.Tests/IntegratedTests/UserUnitTest.cs +++ /dev/null @@ -1,264 +0,0 @@ -using FluentAssertions; -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Tests.Helpers; -using Timeline.Tests.Helpers.Authentication; -using Timeline.Tests.Mock.Data; -using Xunit; -using static Timeline.ErrorCodes.Http.User; - -namespace Timeline.Tests.IntegratedTests -{ - public class UserUnitTest : IClassFixture>, IDisposable - { - private readonly TestApplication _testApp; - private readonly WebApplicationFactory _factory; - - public UserUnitTest(WebApplicationFactory factory) - { - _testApp = new TestApplication(factory); - _factory = _testApp.Factory; - } - - public void Dispose() - { - _testApp.Dispose(); - } - - [Fact] - public async Task Get_Users_List() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.GetAsync("users"); - res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(MockUser.UserInfoList); - } - - [Fact] - public async Task Get_Users_User() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.GetAsync("users/" + MockUser.User.Username); - res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.Should().BeEquivalentTo(MockUser.User.Info); - } - - [Fact] - public async Task Get_Users_404() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.GetAsync("users/usernotexist"); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Get.NotExist); - } - - public static IEnumerable Put_InvalidModel_Data() - { - yield return new object[] { null, false }; - yield return new object[] { "p", null }; - } - - [Theory] - [MemberData(nameof(Put_InvalidModel_Data))] - public async Task Put_InvalidModel(string password, bool? administrator) - { - using var client = await _factory.CreateClientAsAdmin(); - const string url = "users/aaaaaaaa"; - (await client.PutAsJsonAsync(url, - new UserPutRequest { Password = password, Administrator = administrator })) - .Should().BeInvalidModel(); - } - - [Fact] - public async Task Put_BadUsername() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest - { - Password = "???", - Administrator = false - }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Put.BadUsername); - } - - private async Task CheckAdministrator(HttpClient client, string username, bool administrator) - { - var res = await client.GetAsync("users/" + username); - res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() - .Which.Administrator.Should().Be(administrator); - } - - [Fact] - public async Task Put_Modiefied() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePut(false); - await CheckAdministrator(client, MockUser.User.Username, false); - } - - [Fact] - public async Task Put_Created() - { - using var client = await _factory.CreateClientAsAdmin(); - const string username = "puttest"; - const string url = "users/" + username; - - var res = await client.PutAsJsonAsync(url, new UserPutRequest - { - Password = "password", - Administrator = false - }); - res.Should().BePut(true); - await CheckAdministrator(client, username, false); - } - - [Fact] - public async Task Patch_NotExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); - res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Patch.NotExist); - } - - [Fact] - public async Task Patch_Success() - { - using var client = await _factory.CreateClientAsAdmin(); - { - var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username, - new UserPatchRequest { Administrator = false }); - res.Should().HaveStatusCode(200); - await CheckAdministrator(client, MockUser.User.Username, false); - } - } - - [Fact] - public async Task Delete_Deleted() - { - using var client = await _factory.CreateClientAsAdmin(); - var url = "users/" + MockUser.User.Username; - var res = await client.DeleteAsync(url); - res.Should().BeDelete(true); - - var res2 = await client.GetAsync(url); - res2.Should().HaveStatusCode(404); - } - - [Fact] - public async Task Delete_NotExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.DeleteAsync("users/usernotexist"); - res.Should().BeDelete(false); - } - - private const string changeUsernameUrl = "userop/changeusername"; - - public static IEnumerable Op_ChangeUsername_InvalidModel_Data() - { - yield return new[] { null, "uuu" }; - yield return new[] { "uuu", null }; - yield return new[] { "uuu", "???" }; - } - - [Theory] - [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))] - public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername) - { - using var client = await _factory.CreateClientAsAdmin(); - (await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername })) - .Should().BeInvalidModel(); - } - - [Fact] - public async Task Op_ChangeUsername_UserNotExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.NotExist); - } - - [Fact] - public async Task Op_ChangeUsername_UserAlreadyExist() - { - using var client = await _factory.CreateClientAsAdmin(); - var res = await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); - } - - [Fact] - public async Task Op_ChangeUsername_Success() - { - using var client = await _factory.CreateClientAsAdmin(); - const string newUsername = "hahaha"; - var res = await client.PostAsJsonAsync(changeUsernameUrl, - new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername }); - res.Should().HaveStatusCode(200); - await client.CreateUserTokenAsync(newUsername, MockUser.User.Password); - } - - private const string changePasswordUrl = "userop/changepassword"; - - public static IEnumerable Op_ChangePassword_InvalidModel_Data() - { - yield return new[] { null, "ppp" }; - yield return new[] { "ppp", null }; - } - - [Theory] - [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] - public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) - { - using var client = await _factory.CreateClientAsUser(); - (await client.PostAsJsonAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword })) - .Should().BeInvalidModel(); - } - - [Fact] - public async Task Op_ChangePassword_BadOldPassword() - { - using var client = await _factory.CreateClientAsUser(); - var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); - res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() - .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); - } - - [Fact] - public async Task Op_ChangePassword_Success() - { - using var client = await _factory.CreateClientAsUser(); - const string newPassword = "new"; - var res = await client.PostAsJsonAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword }); - res.Should().HaveStatusCode(200); - await client.CreateUserTokenAsync(MockUser.User.Username, newPassword); - } - } -} diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index bc2df469..6b0a9997 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -35,7 +35,7 @@ namespace Timeline.Tests.Mock.Data { Name = user.Username, EncryptedPassword = passwordService.HashPassword(user.Password), - RoleString = UserUtility.IsAdminToRoleString(user.Administrator), + RoleString = UserRoleConvert.ToString(user.Administrator), Avatar = UserAvatar.Create(DateTime.Now) }; } diff --git a/Timeline/Authenticate/Attribute.cs b/Timeline/Authenticate/Attribute.cs deleted file mode 100644 index 239a2a1c..00000000 --- a/Timeline/Authenticate/Attribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Timeline.Entities; - -namespace Timeline.Authenticate -{ - public class AdminAuthorizeAttribute : AuthorizeAttribute - { - public AdminAuthorizeAttribute() - { - Roles = UserRoles.Admin; - } - } - - public class UserAuthorizeAttribute : AuthorizeAttribute - { - public UserAuthorizeAttribute() - { - Roles = UserRoles.User; - } - } -} diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs deleted file mode 100644 index f9409c1a..00000000 --- a/Timeline/Authenticate/AuthHandler.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using System; -using System.Linq; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Services; - -namespace Timeline.Authenticate -{ - static class AuthConstants - { - public const string Scheme = "Bearer"; - public const string DisplayName = "My Jwt Auth Scheme"; - } - - class AuthOptions : AuthenticationSchemeOptions - { - /// - /// The query param key to search for token. If null then query params are not searched for token. Default to "token". - /// - public string TokenQueryParamKey { get; set; } = "token"; - } - - class AuthHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private readonly IUserService _userService; - - public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) - : base(options, logger, encoder, clock) - { - _logger = logger.CreateLogger(); - _userService = userService; - } - - // return null if no token is found - private string ExtractToken() - { - // check the authorization header - string header = Request.Headers[HeaderNames.Authorization]; - if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - var token = header.Substring("Bearer ".Length).Trim(); - _logger.LogInformation("Token is found in authorization header. Token is {} .", token); - return token; - } - - // check the query params - var paramQueryKey = Options.TokenQueryParamKey; - if (!string.IsNullOrEmpty(paramQueryKey)) - { - string token = Request.Query[paramQueryKey]; - if (!string.IsNullOrEmpty(token)) - { - _logger.LogInformation("Token is found in query param with key \"{}\". Token is {} .", paramQueryKey, token); - return token; - } - } - - // not found anywhere then return null - return null; - } - - protected override async Task HandleAuthenticateAsync() - { - var token = ExtractToken(); - if (string.IsNullOrEmpty(token)) - { - _logger.LogInformation("No jwt token is found."); - return AuthenticateResult.NoResult(); - } - - try - { - var userInfo = await _userService.VerifyToken(token); - - var identity = new ClaimsIdentity(AuthConstants.Scheme); - identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserUtility.IsAdminToRoleArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); - - var principal = new ClaimsPrincipal(); - principal.AddIdentity(identity); - - return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme)); - } - catch (ArgumentException) - { - throw; // this exception usually means server error. - } - catch (Exception e) - { - _logger.LogInformation(e, "A jwt token validation failed."); - return AuthenticateResult.Fail(e); - } - } - } -} diff --git a/Timeline/Authenticate/PrincipalExtensions.cs b/Timeline/Authenticate/PrincipalExtensions.cs deleted file mode 100644 index fa39ea89..00000000 --- a/Timeline/Authenticate/PrincipalExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal; -using Timeline.Entities; - -namespace Timeline.Authenticate -{ - public static class PrincipalExtensions - { - public static bool IsAdmin(this IPrincipal principal) - { - return principal.IsInRole(UserRoles.Admin); - } - } -} diff --git a/Timeline/Authentication/Attribute.cs b/Timeline/Authentication/Attribute.cs new file mode 100644 index 00000000..370b37e1 --- /dev/null +++ b/Timeline/Authentication/Attribute.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Timeline.Entities; + +namespace Timeline.Authentication +{ + public class AdminAuthorizeAttribute : AuthorizeAttribute + { + public AdminAuthorizeAttribute() + { + Roles = UserRoles.Admin; + } + } + + public class UserAuthorizeAttribute : AuthorizeAttribute + { + public UserAuthorizeAttribute() + { + Roles = UserRoles.User; + } + } +} diff --git a/Timeline/Authentication/AuthHandler.cs b/Timeline/Authentication/AuthHandler.cs new file mode 100644 index 00000000..47ed1d71 --- /dev/null +++ b/Timeline/Authentication/AuthHandler.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Services; + +namespace Timeline.Authentication +{ + static class AuthConstants + { + public const string Scheme = "Bearer"; + public const string DisplayName = "My Jwt Auth Scheme"; + } + + public class AuthOptions : AuthenticationSchemeOptions + { + /// + /// The query param key to search for token. If null then query params are not searched for token. Default to "token". + /// + public string TokenQueryParamKey { get; set; } = "token"; + } + + public class AuthHandler : AuthenticationHandler + { + private readonly ILogger _logger; + private readonly IUserService _userService; + + public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) + : base(options, logger, encoder, clock) + { + _logger = logger.CreateLogger(); + _userService = userService; + } + + // return null if no token is found + private string? ExtractToken() + { + // check the authorization header + string header = Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase)) + { + var token = header.Substring("Bearer ".Length).Trim(); + _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenFoundInHeader, token); + return token; + } + + // check the query params + var paramQueryKey = Options.TokenQueryParamKey; + if (!string.IsNullOrEmpty(paramQueryKey)) + { + string token = Request.Query[paramQueryKey]; + if (!string.IsNullOrEmpty(token)) + { + _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenFoundInQuery, paramQueryKey, token); + return token; + } + } + + // not found anywhere then return null + return null; + } + + protected override async Task HandleAuthenticateAsync() + { + var token = ExtractToken(); + if (string.IsNullOrEmpty(token)) + { + _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenNotFound); + return AuthenticateResult.NoResult(); + } + + try + { + var userInfo = await _userService.VerifyToken(token); + + var identity = new ClaimsIdentity(AuthConstants.Scheme); + identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); + identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + + return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme)); + } + catch (Exception e) when (e! is ArgumentException) + { + _logger.LogInformation(e, Resources.Authentication.AuthHandler.LogTokenValidationFail); + return AuthenticateResult.Fail(e); + } + } + } +} diff --git a/Timeline/Authentication/PrincipalExtensions.cs b/Timeline/Authentication/PrincipalExtensions.cs new file mode 100644 index 00000000..8d77ab62 --- /dev/null +++ b/Timeline/Authentication/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal; +using Timeline.Entities; + +namespace Timeline.Authentication +{ + internal static class PrincipalExtensions + { + internal static bool IsAdministrator(this IPrincipal principal) + { + return principal.IsInRole(UserRoles.Admin); + } + } +} diff --git a/Timeline/Configs/DatabaseConfig.cs b/Timeline/Configs/DatabaseConfig.cs index e24ecdfb..c9309b08 100644 --- a/Timeline/Configs/DatabaseConfig.cs +++ b/Timeline/Configs/DatabaseConfig.cs @@ -2,6 +2,6 @@ namespace Timeline.Configs { public class DatabaseConfig { - public string ConnectionString { get; set; } + public string ConnectionString { get; set; } = default!; } } diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs index 8c61d7bc..8a17825e 100644 --- a/Timeline/Configs/JwtConfig.cs +++ b/Timeline/Configs/JwtConfig.cs @@ -2,9 +2,9 @@ namespace Timeline.Configs { public class JwtConfig { - public string Issuer { get; set; } - public string Audience { get; set; } - public string SigningKey { get; set; } + public string Issuer { get; set; } = default!; + public string Audience { get; set; } = default!; + public string SigningKey { get; set; } = default!; /// /// Set the default value of expire offset of jwt token. diff --git a/Timeline/Controllers/Testing/TestingAuthController.cs b/Timeline/Controllers/Testing/TestingAuthController.cs index 488a3cff..67b5b2ef 100644 --- a/Timeline/Controllers/Testing/TestingAuthController.cs +++ b/Timeline/Controllers/Testing/TestingAuthController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Timeline.Authenticate; +using Timeline.Authentication; namespace Timeline.Controllers.Testing { diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index e77076ca..5cba1d93 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -6,7 +6,7 @@ using Microsoft.Net.Http.Headers; using System; using System.Linq; using System.Threading.Tasks; -using Timeline.Authenticate; +using Timeline.Authentication; using Timeline.Filters; using Timeline.Models.Http; using Timeline.Services; @@ -106,7 +106,7 @@ namespace Timeline.Controllers return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig, "Content can't be bigger than 10MB.")); - if (!User.IsAdmin() && User.Identity.Name != username) + if (!User.IsAdministrator() && User.Identity.Name != username) { _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); return StatusCode(StatusCodes.Status403Forbidden, @@ -152,7 +152,7 @@ namespace Timeline.Controllers [Authorize] public async Task Delete([FromRoute] string username) { - if (!User.IsAdmin() && User.Identity.Name != username) + if (!User.IsAdministrator() && User.Identity.Name != username) { _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); return StatusCode(StatusCodes.Status403Forbidden, diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index b8d1d659..1771dc85 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -3,10 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using System.Threading.Tasks; -using Timeline.Authenticate; +using Timeline.Authentication; using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; +using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserController; @@ -23,11 +24,6 @@ namespace Timeline public const int NotExist = 10020101; // dd = 01 } - public static class Put // cc = 02 - { - public const int BadUsername = 10020201; // dd = 01 - } - public static class Patch // cc = 03 { public const int NotExist = 10020301; // dd = 01 @@ -78,7 +74,7 @@ namespace Timeline.Controllers } [HttpGet("users/{username}"), AdminAuthorize] - public async Task> Get([FromRoute] string username) + public async Task> Get([FromRoute][Username] string username) { var user = await _userService.GetUser(username); if (user == null) @@ -90,32 +86,24 @@ namespace Timeline.Controllers } [HttpPut("users/{username}"), AdminAuthorize] - public async Task> Put([FromBody] UserPutRequest request, [FromRoute] string username) + public async Task> Put([FromBody] UserPutRequest request, [FromRoute][Username] string username) { - try - { - var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); - switch (result) - { - case PutResult.Create: - _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username))); - return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory)); - case PutResult.Modify: - _logger.LogInformation(Log.Format(LogPutModify, ("Username", username))); - return Ok(CommonPutResponse.Modify(_localizerFactory)); - default: - throw new InvalidBranchException(); - } - } - catch (UsernameBadFormatException e) + var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); + switch (result) { - _logger.LogInformation(e, Log.Format(LogPutBadUsername, ("Username", username))); - return BadRequest(new CommonResponse(ErrorCodes.Http.User.Put.BadUsername, _localizer["ErrorPutBadUsername"])); + case PutResult.Create: + _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username))); + return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory)); + case PutResult.Modify: + _logger.LogInformation(Log.Format(LogPutModify, ("Username", username))); + return Ok(CommonPutResponse.Modify(_localizerFactory)); + default: + throw new InvalidBranchException(); } } [HttpPatch("users/{username}"), AdminAuthorize] - public async Task Patch([FromBody] UserPatchRequest request, [FromRoute] string username) + public async Task Patch([FromBody] UserPatchRequest request, [FromRoute][Username] string username) { try { @@ -130,7 +118,7 @@ namespace Timeline.Controllers } [HttpDelete("users/{username}"), AdminAuthorize] - public async Task> Delete([FromRoute] string username) + public async Task> Delete([FromRoute][Username] string username) { try { diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 550db216..e1b98e7d 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -1,38 +1,7 @@ using Microsoft.EntityFrameworkCore; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Entities { - public static class UserRoles - { - public const string Admin = "admin"; - public const string User = "user"; - } - - [Table("users")] - public class User - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("name"), MaxLength(26), Required] - public string Name { get; set; } = default!; - - [Column("password"), Required] - public string EncryptedPassword { get; set; } = default!; - - [Column("roles"), Required] - public string RoleString { get; set; } = default!; - - [Column("version"), Required] - public long Version { get; set; } - - public UserAvatar? Avatar { get; set; } - - public UserDetailEntity? Detail { get; set; } - } - public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) @@ -41,14 +10,14 @@ namespace Timeline.Entities } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); } - public DbSet Users { get; set; } - public DbSet UserAvatars { get; set; } - public DbSet UserDetails { get; set; } + public DbSet Users { get; set; } = default!; + public DbSet UserAvatars { get; set; } = default!; } } diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs new file mode 100644 index 00000000..6e8e4967 --- /dev/null +++ b/Timeline/Entities/User.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + public static class UserRoles + { + public const string Admin = "admin"; + public const string User = "user"; + } + + [Table("users")] + public class User + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("name"), MaxLength(26), Required] + public string Name { get; set; } = default!; + + [Column("password"), Required] + public string EncryptedPassword { get; set; } = default!; + + [Column("roles"), Required] + public string RoleString { get; set; } = default!; + + [Column("version"), Required] + public long Version { get; set; } + + public UserAvatar? Avatar { get; set; } + } +} diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs deleted file mode 100644 index e02d15c4..00000000 --- a/Timeline/Entities/UserDetail.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Timeline.Entities -{ - [Table("user_details")] - public class UserDetailEntity - { - [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - [Column("nickname"), MaxLength(15)] - public string? Nickname { get; set; } - - [Column("qq"), MaxLength(15)] - public string? QQ { get; set; } - - [Column("email"), MaxLength(50)] - public string? Email { get; set; } - - [Column("phone_number"), MaxLength(15)] - public string? PhoneNumber { get; set; } - - [Column("description")] - public string? Description { get; set; } - - public long UserId { get; set; } - } -} diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index 6c89b230..44ad3af5 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -6,5 +6,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")] diff --git a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs index 3cb561f5..c2252b2c 100644 --- a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs +++ b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs @@ -10,5 +10,10 @@ namespace Timeline.Helpers { return factory.Create(basename, new AssemblyName(typeof(StringLocalizerFactoryExtensions).Assembly.FullName!).Name); } + + internal static StringLocalizer Create(this IStringLocalizerFactory factory) + { + return new StringLocalizer(factory); + } } } \ No newline at end of file diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 98406fec..516c1329 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -20,9 +20,11 @@ namespace Timeline.Models.Http public class ChangeUsernameRequest { [Required] + [Username] public string OldUsername { get; set; } = default!; - [Required, ValidateWith(typeof(UsernameValidator))] + [Required] + [Username] public string NewUsername { get; set; } = default!; } diff --git a/Timeline/Models/UserConvert.cs b/Timeline/Models/UserConvert.cs new file mode 100644 index 00000000..5b132421 --- /dev/null +++ b/Timeline/Models/UserConvert.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; +using Timeline.Services; + +namespace Timeline.Models +{ + public static class UserConvert + { + public static UserInfo CreateUserInfo(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + return new UserInfo(user.Name, UserRoleConvert.ToBool(user.RoleString)); + } + + internal static UserCache CreateUserCache(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + return new UserCache + { + Username = user.Name, + Administrator = UserRoleConvert.ToBool(user.RoleString), + Version = user.Version + }; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs index e502855b..b60bdfa2 100644 --- a/Timeline/Models/UserInfo.cs +++ b/Timeline/Models/UserInfo.cs @@ -12,8 +12,8 @@ namespace Timeline.Models Administrator = administrator; } - public string Username { get; set; } - public bool Administrator { get; set; } + public string Username { get; set; } = default!; + public bool Administrator { get; set; } = default!; public override string ToString() { diff --git a/Timeline/Models/UserUtility.cs b/Timeline/Models/UserUtility.cs deleted file mode 100644 index 405987b5..00000000 --- a/Timeline/Models/UserUtility.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Linq; -using Timeline.Entities; -using Timeline.Services; - -namespace Timeline.Models -{ - public static class UserUtility - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] UserRoleArray { get; } = new string[] { UserRole }; - public static string[] AdminRoleArray { get; } = new string[] { UserRole, AdminRole }; - - public static string[] IsAdminToRoleArray(bool isAdmin) - { - return isAdmin ? AdminRoleArray : UserRoleArray; - } - - public static bool RoleArrayToIsAdmin(string[] roles) - { - return roles.Contains(AdminRole); - } - - public static string[] RoleStringToRoleArray(string roleString) - { - return roleString.Split(',').ToArray(); - } - - public static string RoleArrayToRoleString(string[] roles) - { - return string.Join(',', roles); - } - - public static string IsAdminToRoleString(bool isAdmin) - { - return RoleArrayToRoleString(IsAdminToRoleArray(isAdmin)); - } - - public static bool RoleStringToIsAdmin(string roleString) - { - return RoleArrayToIsAdmin(RoleStringToRoleArray(roleString)); - } - - public static UserInfo CreateUserInfo(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - return new UserInfo(user.Name, RoleStringToIsAdmin(user.RoleString)); - } - - internal static UserCache CreateUserCache(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - return new UserCache { Username = user.Name, Administrator = RoleStringToIsAdmin(user.RoleString), Version = user.Version }; - } - } -} diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index 65d4da71..dc237add 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; namespace Timeline.Models.Validation { @@ -36,4 +37,15 @@ namespace Timeline.Models.Validation return (true, SuccessMessageGenerator); } } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, + AllowMultiple = false)] + public class UsernameAttribute : ValidateWithAttribute + { + public UsernameAttribute() + : base(typeof(UsernameValidator)) + { + + } + } } diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index 606ba7b4..d2c7c377 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -8,7 +8,7 @@ namespace Timeline.Models.Validation { /// /// Generate a message from a localizer factory. - /// If localizerFactory is null, it should return a neutral-cultural message. + /// If localizerFactory is null, it should return a culture-invariant message. /// /// The localizer factory. Could be null. /// The message. diff --git a/Timeline/Program.cs b/Timeline/Program.cs index 7474fe2f..4a098adf 100644 --- a/Timeline/Program.cs +++ b/Timeline/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; +using System.Resources; namespace Timeline { diff --git a/Timeline/Resources/Authentication/AuthHandler.Designer.cs b/Timeline/Resources/Authentication/AuthHandler.Designer.cs new file mode 100644 index 00000000..fd4540ea --- /dev/null +++ b/Timeline/Resources/Authentication/AuthHandler.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Authentication { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AuthHandler { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AuthHandler() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Authentication.AuthHandler", typeof(AuthHandler).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Token is found in authorization header. Token is {0} .. + /// + internal static string LogTokenFoundInHeader { + get { + return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} .. + /// + internal static string LogTokenFoundInQuery { + get { + return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No jwt token is found.. + /// + internal static string LogTokenNotFound { + get { + return ResourceManager.GetString("LogTokenNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A jwt token validation failed.. + /// + internal static string LogTokenValidationFail { + get { + return ResourceManager.GetString("LogTokenValidationFail", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Authentication/AuthHandler.resx b/Timeline/Resources/Authentication/AuthHandler.resx new file mode 100644 index 00000000..4cddc8ce --- /dev/null +++ b/Timeline/Resources/Authentication/AuthHandler.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Token is found in authorization header. Token is {0} . + + + Token is found in query param with key "{0}". Token is {1} . + + + No jwt token is found. + + + A jwt token validation failed. + + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 15a8169e..24f6b8e6 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -69,6 +69,69 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server.. + /// + internal static string HashedPasswordBadFromatException { + get { + return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not of valid base64 format. See inner exception.. + /// + internal static string HashedPasswordBadFromatExceptionNotBase64 { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Decoded hashed password is of length 0.. + /// + internal static string HashedPasswordBadFromatExceptionNotLength0 { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to See inner exception.. + /// + internal static string HashedPasswordBadFromatExceptionNotOthers { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Salt length < 128 bits.. + /// + internal static string HashedPasswordBadFromatExceptionNotSaltTooShort { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subkey length < 128 bits.. + /// + internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown format marker.. + /// + internal static string HashedPasswordBadFromatExceptionNotUnknownMarker { + get { + return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", resourceCulture); + } + } + /// /// Looks up a localized string similar to The version of the jwt token is old.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index af771393..408c45a1 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -120,6 +120,27 @@ The password is wrong. + + The hashes password is of bad format. It might not be created by server. + + + Not of valid base64 format. See inner exception. + + + Decoded hashed password is of length 0. + + + See inner exception. + + + Salt length < 128 bits. + + + Subkey length < 128 bits. + + + Unknown format marker. + The version of the jwt token is old. diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs new file mode 100644 index 00000000..2a04dded --- /dev/null +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserService", typeof(UserService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to New username is of bad format.. + /// + internal static string ExceptionNewUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionNewUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Old username is of bad format.. + /// + internal static string ExceptionOldUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionOldUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A cache entry is created.. + /// + internal static string LogCacheCreate { + get { + return ResourceManager.GetString("LogCacheCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A cache entry is removed.. + /// + internal static string LogCacheRemove { + get { + return ResourceManager.GetString("LogCacheRemove", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new user entry is added to the database.. + /// + internal static string LogDatabaseCreate { + get { + return ResourceManager.GetString("LogDatabaseCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user entry is removed from the database.. + /// + internal static string LogDatabaseRemove { + get { + return ResourceManager.GetString("LogDatabaseRemove", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user entry is updated to the database.. + /// + internal static string LogDatabaseUpdate { + get { + return ResourceManager.GetString("LogDatabaseUpdate", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx new file mode 100644 index 00000000..3670d8f9 --- /dev/null +++ b/Timeline/Resources/Services/UserService.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + New username is of bad format. + + + Old username is of bad format. + + + A cache entry is created. + + + A cache entry is removed. + + + A new user entry is added to the database. + + + A user entry is removed from the database. + + + A user entry is updated to the database. + + \ No newline at end of file diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs index e09a1365..e04a861b 100644 --- a/Timeline/Services/PasswordService.cs +++ b/Timeline/Services/PasswordService.cs @@ -12,13 +12,23 @@ namespace Timeline.Services [Serializable] public class HashedPasswordBadFromatException : Exception { - public HashedPasswordBadFromatException(string hashedPassword, string message) : base(message) { HashedPassword = hashedPassword; } - public HashedPasswordBadFromatException(string hashedPassword, string message, Exception inner) : base(message, inner) { HashedPassword = hashedPassword; } + private static string MakeMessage(string reason) + { + return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason; + } + + public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { } + + public HashedPasswordBadFromatException(string message) : base(message) { } + public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { } + + public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; } + public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; } protected HashedPasswordBadFromatException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - public string HashedPassword { get; private set; } + public string? HashedPassword { get; set; } } public interface IPasswordService @@ -140,22 +150,20 @@ namespace Timeline.Services } catch (FormatException e) { - throw new HashedPasswordBadFromatException(hashedPassword, "Not of valid base64 format. See inner exception.", e); + throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e); } // read the format marker from the hashed password if (decodedHashedPassword.Length == 0) { - throw new HashedPasswordBadFromatException(hashedPassword, "Decoded hashed password is of length 0."); + throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0); } - switch (decodedHashedPassword[0]) - { - case 0x01: - return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword); - default: - throw new HashedPasswordBadFromatException(hashedPassword, "Unknown format marker."); - } + return (decodedHashedPassword[0]) switch + { + 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword), + _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker), + }; } private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString) @@ -170,7 +178,7 @@ namespace Timeline.Services // Read the salt: must be >= 128 bits if (saltLength < 128 / 8) { - throw new HashedPasswordBadFromatException(hashedPasswordString, "Salt length < 128 bits."); + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort); } byte[] salt = new byte[saltLength]; Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); @@ -179,7 +187,7 @@ namespace Timeline.Services int subkeyLength = hashedPassword.Length - 13 - salt.Length; if (subkeyLength < 128 / 8) { - throw new HashedPasswordBadFromatException(hashedPasswordString, "Subkey length < 128 bits."); + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort); } byte[] expectedSubkey = new byte[subkeyLength]; Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); @@ -193,7 +201,7 @@ namespace Timeline.Services // This should never occur except in the case of a malformed payload, where // we might go off the end of the array. Regardless, a malformed payload // implies verification failed. - throw new HashedPasswordBadFromatException(hashedPasswordString, "See inner exception.", e); + throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e); } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 45ef8a5c..d706d05e 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,15 +1,13 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; -using static Timeline.Helpers.MyLogHelper; -using static Timeline.Models.UserUtility; namespace Timeline.Services { @@ -30,6 +28,7 @@ namespace Timeline.Services /// The expired time point. Null then use default. See for what is default. /// An containing the created token and user info. /// Thrown when or is null. + /// Thrown when username is of bad format. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. Task CreateToken(string username, string password, DateTime? expires = null); @@ -50,6 +49,8 @@ namespace Timeline.Services /// /// Username of the user. /// The info of the user. Null if the user of given username does not exists. + /// Thrown when is null. + /// Thrown when is of bad format. Task GetUser(string username); /// @@ -82,6 +83,7 @@ namespace Timeline.Services /// New password. Null if not modify. /// Whether the user is administrator. Null if not modify. /// Thrown if is null. + /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. Task PatchUser(string username, string? password, bool? administrator); @@ -90,6 +92,7 @@ namespace Timeline.Services /// /// Username of thet user to delete. Can't be null. /// Thrown if is null. + /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. Task DeleteUser(string username); @@ -100,6 +103,7 @@ namespace Timeline.Services /// The user's old password. /// The user's new password. /// Thrown if or or is null. + /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. /// Thrown if the old password is wrong. Task ChangePassword(string username, string oldPassword, string newPassword); @@ -109,9 +113,9 @@ namespace Timeline.Services /// /// The user's old username. /// The new username. - /// Thrown if or is null or empty. + /// Thrown if or is null. /// Thrown if the user with old username does not exist. - /// Thrown if the new username is not accepted because of bad format. + /// Thrown if the or is of bad format. /// Thrown if user with the new username already exists. Task ChangeUsername(string oldUsername, string newUsername); } @@ -157,7 +161,19 @@ namespace Timeline.Services { var key = GenerateCacheKeyByUserId(id); _memoryCache.Remove(key); - _logger.LogInformation(FormatLogMessage("A cache entry is removed.", Pair("Key", key))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key))); + } + + private void CheckUsernameFormat(string username, string? message = null) + { + var (result, messageGenerator) = _usernameValidator.Validate(username); + if (!result) + { + if (message == null) + throw new UsernameBadFormatException(username, messageGenerator(null)); + else + throw new UsernameBadFormatException(username, message + messageGenerator(null)); + } } public async Task CreateToken(string username, string password, DateTime? expires) @@ -166,6 +182,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username)); if (password == null) throw new ArgumentNullException(nameof(password)); + CheckUsernameFormat(username); // We need password info, so always check the database. var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); @@ -185,7 +202,7 @@ namespace Timeline.Services return new CreateTokenResult { Token = token, - User = CreateUserInfo(user) + User = UserConvert.CreateUserInfo(user) }; } @@ -208,9 +225,9 @@ namespace Timeline.Services throw new UserNotExistException(id); // create cache - cache = CreateUserCache(user); + cache = UserConvert.CreateUserCache(user); _memoryCache.CreateEntry(key).SetValue(cache); - _logger.LogInformation(FormatLogMessage("A cache entry is created.", Pair("Key", key))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key))); } if (tokenInfo.Version != cache.Version) @@ -221,16 +238,20 @@ namespace Timeline.Services public async Task GetUser(string username) { + if (username == null) + throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username); + return await _databaseContext.Users .Where(user => user.Name == username) - .Select(user => CreateUserInfo(user)) + .Select(user => UserConvert.CreateUserInfo(user)) .SingleOrDefaultAsync(); } public async Task ListUsers() { return await _databaseContext.Users - .Select(user => CreateUserInfo(user)) + .Select(user => UserConvert.CreateUserInfo(user)) .ToArrayAsync(); } @@ -240,12 +261,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username)); if (password == null) throw new ArgumentNullException(nameof(password)); - - var (result, messageGenerator) = _usernameValidator.Validate(username); - if (!result) - { - throw new UsernameBadFormatException(username, messageGenerator(null)); - } + CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); @@ -255,20 +271,22 @@ namespace Timeline.Services { Name = username, EncryptedPassword = _passwordService.HashPassword(password), - RoleString = IsAdminToRoleString(administrator), + RoleString = UserRoleConvert.ToString(administrator), Avatar = UserAvatar.Create(DateTime.Now) }; await _databaseContext.AddAsync(newUser); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(FormatLogMessage("A new user entry is added to the database.", Pair("Id", newUser.Id))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate, + ("Id", newUser.Id), ("Username", username), ("Administrator", administrator))); return PutResult.Create; } user.EncryptedPassword = _passwordService.HashPassword(password); - user.RoleString = IsAdminToRoleString(administrator); + user.RoleString = UserRoleConvert.ToString(administrator); user.Version += 1; await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, + ("Id", user.Id), ("Username", username), ("Administrator", administrator))); //clear cache RemoveCache(user.Id); @@ -276,10 +294,11 @@ namespace Timeline.Services return PutResult.Modify; } - public async Task PatchUser(string username, string password, bool? administrator) + public async Task PatchUser(string username, string? password, bool? administrator) { if (username == null) throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) @@ -292,12 +311,12 @@ namespace Timeline.Services if (administrator != null) { - user.RoleString = IsAdminToRoleString(administrator.Value); + user.RoleString = UserRoleConvert.ToString(administrator.Value); } user.Version += 1; await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id))); + _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id)); //clear cache RemoveCache(user.Id); @@ -307,6 +326,7 @@ namespace Timeline.Services { if (username == null) throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) @@ -314,7 +334,8 @@ namespace Timeline.Services _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(FormatLogMessage("A user entry is removed from the database.", Pair("Id", user.Id))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove, + ("Id", user.Id))); //clear cache RemoveCache(user.Id); @@ -328,6 +349,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(oldPassword)); if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); + CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) @@ -340,23 +362,20 @@ namespace Timeline.Services user.EncryptedPassword = _passwordService.HashPassword(newPassword); user.Version += 1; await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, + ("Id", user.Id), ("Operation", "Change password"))); //clear cache RemoveCache(user.Id); } public async Task ChangeUsername(string oldUsername, string newUsername) { - if (string.IsNullOrEmpty(oldUsername)) - throw new ArgumentException("Old username is null or empty", nameof(oldUsername)); - if (string.IsNullOrEmpty(newUsername)) - throw new ArgumentException("New username is null or empty", nameof(newUsername)); - - - var (result, messageGenerator) = _usernameValidator.Validate(newUsername); - if (!result) - { - throw new UsernameBadFormatException(newUsername, $"New username is of bad format. {messageGenerator(null)}"); - } + if (oldUsername == null) + throw new ArgumentNullException(nameof(oldUsername)); + if (newUsername == null) + throw new ArgumentNullException(nameof(newUsername)); + CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat); + CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat); var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync(); if (user == null) @@ -369,8 +388,8 @@ namespace Timeline.Services user.Name = newUsername; user.Version += 1; await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(FormatLogMessage("A user entry changed name field.", - Pair("Id", user.Id), Pair("Old Username", oldUsername), Pair("New Username", newUsername))); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, + ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername))); RemoveCache(user.Id); } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index be5bce7c..d54ea6ca 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Globalization; -using Timeline.Authenticate; +using Timeline.Authentication; using Timeline.Configs; using Timeline.Entities; using Timeline.Helpers; diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index e29c4e4b..0ba34471 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -34,6 +34,11 @@ + + True + True + AuthHandler.resx + True True @@ -64,9 +69,18 @@ True Exception.resx + + True + True + UserService.resx + + + ResXFileCodeGenerator + AuthHandler.Designer.cs + ResXFileCodeGenerator Common.Designer.cs @@ -102,5 +116,9 @@ ResXFileCodeGenerator Exception.Designer.cs + + ResXFileCodeGenerator + UserService.Designer.cs + -- cgit v1.2.3