aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline.Tests/Controllers/TokenControllerTest.cs6
-rw-r--r--Timeline.Tests/Controllers/UserControllerTest.cs6
-rw-r--r--Timeline.Tests/IntegratedTests/UserDetailTest.cs145
-rw-r--r--Timeline.Tests/UserDetailServiceTest.cs271
-rw-r--r--Timeline.Tests/UserDetailValidatorTest.cs97
-rw-r--r--Timeline.Tests/UsernameValidatorUnitTest.cs7
-rw-r--r--Timeline/Controllers/TokenController.cs38
-rw-r--r--Timeline/Controllers/UserController.cs31
-rw-r--r--Timeline/Controllers/UserDetailController.cs96
-rw-r--r--Timeline/Models/Http/Common.cs8
-rw-r--r--Timeline/Models/PutResult.cs4
-rw-r--r--Timeline/Models/UserDetail.cs45
-rw-r--r--Timeline/Models/Validation/UserDetailValidator.cs116
-rw-r--r--Timeline/Models/Validation/UsernameValidator.cs5
-rw-r--r--Timeline/Models/Validation/Validator.cs44
-rw-r--r--Timeline/Resources/Controllers/TokenController.Designer.cs153
-rw-r--r--Timeline/Resources/Controllers/UserController.Designer.cs171
-rw-r--r--Timeline/Resources/Models/Http/Common.en.resx (renamed from Timeline/Resources/Http/Common.en.resx)0
-rw-r--r--Timeline/Resources/Models/Http/Common.zh.resx (renamed from Timeline/Resources/Http/Common.zh.resx)0
-rw-r--r--Timeline/Resources/Models/Validation/Validator.Designer.cs81
-rw-r--r--Timeline/Resources/Models/Validation/Validator.en.resx129
-rw-r--r--Timeline/Resources/Models/Validation/Validator.resx126
-rw-r--r--Timeline/Resources/Models/Validation/Validator.zh.resx129
-rw-r--r--Timeline/Resources/Services/Exception.Designer.cs189
-rw-r--r--Timeline/Resources/Services/Exception.resx162
-rw-r--r--Timeline/Services/BadPasswordException.cs27
-rw-r--r--Timeline/Services/JwtBadVersionException.cs36
-rw-r--r--Timeline/Services/JwtService.cs76
-rw-r--r--Timeline/Services/JwtVerifyException.cs59
-rw-r--r--Timeline/Services/UserDetailService.cs135
-rw-r--r--Timeline/Services/UserNotExistException.cs41
-rw-r--r--Timeline/Services/UserService.cs195
-rw-r--r--Timeline/Services/UsernameBadFormatException.cs27
-rw-r--r--Timeline/Services/UsernameConfictException.cs25
-rw-r--r--Timeline/Startup.cs1
-rw-r--r--Timeline/Timeline.csproj34
36 files changed, 1504 insertions, 1211 deletions
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<object[]> 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<WebApplicationFactory<Startup>>, IDisposable
- {
- private readonly TestApplication _testApp;
- private readonly WebApplicationFactory<Startup> _factory;
-
- public UserDetailTest(WebApplicationFactory<Startup> 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<UserDetail>()
- .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<UserDetail>()
- .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<UserDetailService>.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<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.GetUserNickname("")).Should().Throw<ArgumentException>()
- .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<UserNotExistException>()
- .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<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.GetUserDetail("")).Should().Throw<ArgumentException>()
- .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<UserNotExistException>()
- .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<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw<ArgumentException>()
- .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<UserNotExistException>()
- .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<TValidator>(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<TValidator>(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<UserDetailValidators.QQValidator>(qq);
- }
-
- [Theory]
- [InlineData(123, "type")]
- [InlineData("123", "short")]
- [InlineData("111111111111111111111111111111111111", "long")]
- [InlineData("aaaaaaaa", "digit")]
- public void Fail(object qq, string messageContains)
- {
- FailWith<UserDetailValidators.QQValidator>(qq, messageContains);
- }
- }
-
- public class EMail
- {
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- [InlineData("aaa@aaa.net")]
- public void Success(object email)
- {
- SucceedWith<UserDetailValidators.EMailValidator>(email);
- }
-
- public static IEnumerable<object[]> 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<UserDetailValidators.EMailValidator>(email, messageContains);
- }
- }
-
- public class PhoneNumber
- {
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- [InlineData("12345678910")]
- public void Success(object phoneNumber)
- {
- SucceedWith<UserDetailValidators.PhoneNumberValidator>(phoneNumber);
- }
-
- [Theory]
- [InlineData(123, "type")]
- [InlineData("111111111111111111111111111111111111", "long")]
- [InlineData("aaaaaaaa", "digit")]
- public void Fail(object phoneNumber, string messageContains)
- {
- FailWith<UserDetailValidators.PhoneNumberValidator>(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<UserDetailController> _logger;
- private readonly IUserDetailService _service;
-
- public UserDetailController(ILogger<UserDetailController> logger, IUserDetailService service)
- {
- _logger = logger;
- _service = service;
- }
-
- [HttpGet("nickname")]
- [UserAuthorize]
- [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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
/// <summary>
/// Indicates the item did not exist and now is created.
/// </summary>
- Created,
+ Create,
/// <summary>
/// Indicates the item exists already and is modified.
/// </summary>
- 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
{
/// <summary>
/// A validator to validate value.
- /// See <see cref="Validate(object, out string)"/>.
+ /// See <see cref="Validate(object?, out string)"/>.
/// </summary>
public interface IValidator
{
@@ -15,7 +18,7 @@ namespace Timeline.Models.Validation
/// <param name="value">The value to validate.</param>
/// <param name="message">The validation message.</param>
/// <returns>True if validation passed. Otherwise false.</returns>
- 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
/// </remarks>
public abstract class Validator<T> : IValidator
{
- public bool Validate(object value, out string message)
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>")]
+ 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<IStringLocalizerFactory>();
+ 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 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// 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.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // 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() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [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;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The password is wrong..
+ /// </summary>
+ internal static string LogBadPassword {
+ get {
+ return ResourceManager.GetString("LogBadPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user failed to create a token..
+ /// </summary>
+ internal static string LogCreateFailure {
+ get {
+ return ResourceManager.GetString("LogCreateFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user succeeded to create a token..
+ /// </summary>
+ internal static string LogCreateSuccess {
+ get {
+ return ResourceManager.GetString("LogCreateSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user does not exist..
+ /// </summary>
+ internal static string LogUserNotExist {
+ get {
+ return ResourceManager.GetString("LogUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad format. It might not be created by the server..
+ /// </summary>
+ internal static string LogVerifyBadFormat {
+ get {
+ return ResourceManager.GetString("LogVerifyBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is expired..
+ /// </summary>
+ internal static string LogVerifyExpire {
+ get {
+ return ResourceManager.GetString("LogVerifyExpire", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A token failed to be verified..
+ /// </summary>
+ internal static string LogVerifyFailure {
+ get {
+ return ResourceManager.GetString("LogVerifyFailure", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token has an old version. User might have update some info..
+ /// </summary>
+ internal static string LogVerifyOldVersion {
+ get {
+ return ResourceManager.GetString("LogVerifyOldVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A token succeeded to be verified..
+ /// </summary>
+ internal static string LogVerifySuccess {
+ get {
+ return ResourceManager.GetString("LogVerifySuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user..
+ /// </summary>
+ 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 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// 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.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Controllers {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // 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() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [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;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change password with wrong old password failed..
+ /// </summary>
+ internal static string LogChangePasswordBadPassword {
+ get {
+ return ResourceManager.GetString("LogChangePasswordBadPassword", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has changed password..
+ /// </summary>
+ internal static string LogChangePasswordSuccess {
+ get {
+ return ResourceManager.GetString("LogChangePasswordSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change a user&apos;s username to a existent one failed..
+ /// </summary>
+ internal static string LogChangeUsernameAlreadyExist {
+ get {
+ return ResourceManager.GetString("LogChangeUsernameAlreadyExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to change a username of a user that does not exist failed..
+ /// </summary>
+ internal static string LogChangeUsernameNotExist {
+ get {
+ return ResourceManager.GetString("LogChangeUsernameNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has changed username..
+ /// </summary>
+ internal static string LogChangeUsernameSuccess {
+ get {
+ return ResourceManager.GetString("LogChangeUsernameSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has been deleted..
+ /// </summary>
+ internal static string LogDeleteDelete {
+ get {
+ return ResourceManager.GetString("LogDeleteDelete", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a user that does not exist..
+ /// </summary>
+ internal static string LogDeleteNotExist {
+ get {
+ return ResourceManager.GetString("LogDeleteNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to retrieve info of a user that does not exist failed..
+ /// </summary>
+ internal static string LogGetUserNotExist {
+ get {
+ return ResourceManager.GetString("LogGetUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to patch a user that does not exist failed..
+ /// </summary>
+ internal static string LogPatchUserNotExist {
+ get {
+ return ResourceManager.GetString("LogPatchUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to create a user with bad username failed..
+ /// </summary>
+ internal static string LogPutBadUsername {
+ get {
+ return ResourceManager.GetString("LogPutBadUsername", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has been created..
+ /// </summary>
+ internal static string LogPutCreate {
+ get {
+ return ResourceManager.GetString("LogPutCreate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user has been modified..
+ /// </summary>
+ internal static string LogPutModify {
+ get {
+ return ResourceManager.GetString("LogPutModify", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx
index 40d44191..40d44191 100644
--- a/Timeline/Resources/Http/Common.en.resx
+++ b/Timeline/Resources/Models/Http/Common.en.resx
diff --git a/Timeline/Resources/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx
index b6d955d9..b6d955d9 100644
--- a/Timeline/Resources/Http/Common.zh.resx
+++ b/Timeline/Resources/Models/Http/Common.zh.resx
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 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// 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.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // 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() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [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;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to create a validator instance from default constructor. See inner exception..
+ /// </summary>
+ internal static string ValidateWithAttributeCreateFail {
+ get {
+ return ResourceManager.GetString("ValidateWithAttributeCreateFail", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Given type is not assignable to IValidator..
+ /// </summary>
+ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ValidatorMessageBadType" xml:space="preserve">
+ <value>Value is not of type {0}.</value>
+ </data>
+ <data name="ValidatorMessageNull" xml:space="preserve">
+ <value>Value can't be null.</value>
+ </data>
+ <data name="ValidatorMessageSuccess" xml:space="preserve">
+ <value>Validation succeeded.</value>
+ </data>
+</root> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ValidateWithAttributeCreateFail" xml:space="preserve">
+ <value>Failed to create a validator instance from default constructor. See inner exception.</value>
+ </data>
+ <data name="ValidateWithAttributeNotValidator" xml:space="preserve">
+ <value>Given type is not assignable to IValidator.</value>
+ </data>
+</root> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="ValidatorMessageBadType" xml:space="preserve">
+ <value>值不是类型{0}的实例。</value>
+ </data>
+ <data name="ValidatorMessageNull" xml:space="preserve">
+ <value>值不能为null.</value>
+ </data>
+ <data name="ValidatorMessageSuccess" xml:space="preserve">
+ <value>验证成功。</value>
+ </data>
+</root> \ 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 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// 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.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Services {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // 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() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [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;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The password is wrong..
+ /// </summary>
+ internal static string BadPasswordException {
+ get {
+ return ResourceManager.GetString("BadPasswordException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The version of the jwt token is old..
+ /// </summary>
+ internal static string JwtBadVersionException {
+ get {
+ return ResourceManager.GetString("JwtBadVersionException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token didn&apos;t pass verification because {0}, see inner exception for information..
+ /// </summary>
+ internal static string JwtVerifyException {
+ get {
+ return ResourceManager.GetString("JwtVerifyException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to token is expired..
+ /// </summary>
+ internal static string JwtVerifyExceptionExpired {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionExpired", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to id claim is not a number..
+ /// </summary>
+ internal static string JwtVerifyExceptionIdClaimBadFormat {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionIdClaimBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to id claim does not exist..
+ /// </summary>
+ internal static string JwtVerifyExceptionNoIdClaim {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionNoIdClaim", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to version claim does not exist..
+ /// </summary>
+ internal static string JwtVerifyExceptionNoVersionClaim {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionNoVersionClaim", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to version of token is old..
+ /// </summary>
+ internal static string JwtVerifyExceptionOldVersion {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionOldVersion", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to uncommon error..
+ /// </summary>
+ internal static string JwtVerifyExceptionOthers {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionOthers", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown error code..
+ /// </summary>
+ internal static string JwtVerifyExceptionUnknown {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionUnknown", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to version claim is not a number..
+ /// </summary>
+ internal static string JwtVerifyExceptionVersionClaimBadFormat {
+ get {
+ return ResourceManager.GetString("JwtVerifyExceptionVersionClaimBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The username is of bad format..
+ /// </summary>
+ internal static string UsernameBadFormatException {
+ get {
+ return ResourceManager.GetString("UsernameBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The username already exists..
+ /// </summary>
+ internal static string UsernameConfictException {
+ get {
+ return ResourceManager.GetString("UsernameConfictException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The user does not exist..
+ /// </summary>
+ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="BadPasswordException" xml:space="preserve">
+ <value>The password is wrong.</value>
+ </data>
+ <data name="JwtBadVersionException" xml:space="preserve">
+ <value>The version of the jwt token is old.</value>
+ </data>
+ <data name="JwtVerifyException" xml:space="preserve">
+ <value>The token didn't pass verification because {0}, see inner exception for information.</value>
+ </data>
+ <data name="JwtVerifyExceptionExpired" xml:space="preserve">
+ <value>token is expired.</value>
+ </data>
+ <data name="JwtVerifyExceptionIdClaimBadFormat" xml:space="preserve">
+ <value>id claim is not a number.</value>
+ </data>
+ <data name="JwtVerifyExceptionNoIdClaim" xml:space="preserve">
+ <value>id claim does not exist.</value>
+ </data>
+ <data name="JwtVerifyExceptionNoVersionClaim" xml:space="preserve">
+ <value>version claim does not exist.</value>
+ </data>
+ <data name="JwtVerifyExceptionOldVersion" xml:space="preserve">
+ <value>version of token is old.</value>
+ </data>
+ <data name="JwtVerifyExceptionOthers" xml:space="preserve">
+ <value>uncommon error.</value>
+ </data>
+ <data name="JwtVerifyExceptionUnknown" xml:space="preserve">
+ <value>unknown error code.</value>
+ </data>
+ <data name="JwtVerifyExceptionVersionClaimBadFormat" xml:space="preserve">
+ <value>version claim is not a number.</value>
+ </data>
+ <data name="UsernameBadFormatException" xml:space="preserve">
+ <value>The username is of bad format.</value>
+ </data>
+ <data name="UsernameConfictException" xml:space="preserve">
+ <value>The username already exists.</value>
+ </data>
+ <data name="UserNotExistException" xml:space="preserve">
+ <value>The user does not exist.</value>
+ </data>
+</root> \ 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) { }
+
+ /// <summary>
+ /// The wrong password.
+ /// </summary>
+ 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) { }
+
+ /// <summary>
+ /// The version in the token.
+ /// </summary>
+ public long? TokenVersion { get; set; }
+
+ /// <summary>
+ /// The version required.
+ /// </summary>
+ 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;
-
- /// <summary>
- /// Corresponds to <see cref="SecurityTokenExpiredException"/>.
- /// </summary>
- 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
{
/// <summary>
@@ -89,7 +33,7 @@ namespace Timeline.Services
/// <param name="token">The token string to verify.</param>
/// <returns>Return the saved info in token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtTokenVerifyException">Thrown when the token is invalid.</exception>
+ /// <exception cref="JwtVerifyException">Thrown when the token is invalid.</exception>
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;
+
+ /// <summary>
+ /// Corresponds to <see cref="SecurityTokenExpiredException"/>.
+ /// </summary>
+ 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
- {
- /// <summary>
- /// Get the nickname of user.
- /// </summary>
- /// <param name="username">The username to get nickname of.</param>
- /// <returns>The user's nickname. Null if not set.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task<string> GetUserNickname(string username);
-
- /// <summary>
- /// Get the detail of user.
- /// </summary>
- /// <param name="username">The username to get user detail of.</param>
- /// <returns>The user detail.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task<UserDetail> GetUserDetail(string username);
-
- /// <summary>
- /// Update the detail of user. This function does not do data check.
- /// </summary>
- /// <param name="username">The username to get user detail of.</param>
- /// <param name="detail">The detail to update. Can't be null. Any null member means not set.</param>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty or <paramref name="detail"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
- Task UpdateUserDetail(string username, UserDetail detail);
- }
-
- public class UserDetailService : IUserDetailService
- {
- private readonly ILogger<UserDetailService> _logger;
-
- private readonly DatabaseContext _databaseContext;
-
- public UserDetailService(ILogger<UserDetailService> logger, DatabaseContext databaseContext)
- {
- _logger = logger;
- _databaseContext = databaseContext;
- }
-
- private async Task<UserDetailEntity> 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<UserDetailEntity> 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<string> 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<UserDetail> 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<IUserDetailService, UserDetailService>();
- }
- }
-}
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
+{
+ /// <summary>
+ /// The user requested does not exist.
+ /// </summary>
+ [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) { }
+
+ /// <summary>
+ /// The username of the user that does not exist.
+ /// </summary>
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// The id of the user that does not exist.
+ /// </summary>
+ 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) { }
-
- /// <summary>
- /// The username that does not exist.
- /// </summary>
- public string Username { get; set; }
-
- /// <summary>
- /// The id that does not exist.
- /// </summary>
- 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) { }
-
- /// <summary>
- /// The wrong password.
- /// </summary>
- 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) { }
-
- /// <summary>
- /// The version in the token.
- /// </summary>
- public long? TokenVersion { get; set; }
-
- /// <summary>
- /// The version required.
- /// </summary>
- public long? RequiredVersion { get; set; }
- }
-
- /// <summary>
- /// Thrown when username is of bad format.
- /// </summary>
- [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) { }
-
- /// <summary>
- /// Username of bad format.
- /// </summary>
- public string Username { get; private set; }
- }
-
-
- /// <summary>
- /// Thrown when the user already exists.
- /// </summary>
- [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) { }
-
- /// <summary>
- /// The username that already exists.
- /// </summary>
- 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
/// <param name="token">The token to verify.</param>
/// <returns>The user info specified by the token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
+ /// <exception cref="JwtVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
/// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
- /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
Task<UserInfo> VerifyToken(string token);
/// <summary>
@@ -221,10 +65,12 @@ namespace Timeline.Services
/// <param name="username">Username of user.</param>
/// <param name="password">Password of user.</param>
/// <param name="administrator">Whether the user is administrator.</param>
- /// <returns>Return <see cref="PutResult.Created"/> if a new user is created.
- /// Return <see cref="PutResult.Modified"/> if a existing user is modified.</returns>
- /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <returns>
+ /// Return <see cref="PutResult.Create"/> if a new user is created.
+ /// Return <see cref="PutResult.Modify"/> if a existing user is modified.
+ /// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
Task<PutResult> PutUser(string username, string password, bool administrator);
/// <summary>
@@ -237,7 +83,7 @@ namespace Timeline.Services
/// <param name="administrator">Whether the user is administrator. Null if not modify.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
/// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- Task PatchUser(string username, string password, bool? administrator);
+ Task PatchUser(string username, string? password, bool? administrator);
/// <summary>
/// Delete a user of given username.
@@ -266,13 +112,13 @@ namespace Timeline.Services
/// <exception cref="ArgumentException">Thrown if <paramref name="oldUsername"/> or <paramref name="newUsername"/> is null or empty.</exception>
/// <exception cref="UserNotExistException">Thrown if the user with old username does not exist.</exception>
/// <exception cref="UsernameBadFormatException">Thrown if the new username is not accepted because of bad format.</exception>
- /// <exception cref="UserAlreadyExistException">Thrown if user with the new username already exists.</exception>
+ /// <exception cref="UsernameConfictException">Thrown if user with the new username already exists.</exception>
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<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService)
+ private readonly IStringLocalizerFactory _localizerFactory;
+
+ public UserService(ILogger<UserService> 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
+{
+ /// <summary>
+ /// Thrown when username is of bad format.
+ /// </summary>
+ [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) { }
+
+ /// <summary>
+ /// Username of bad format.
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Thrown when the user already exists.
+ /// </summary>
+ [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) { }
+
+ /// <summary>
+ /// The username that already exists.
+ /// </summary>
+ 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<IClock, Clock>();
services.AddUserAvatarService();
- services.AddUserDetailService();
var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>();
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 @@
<AutoGen>True</AutoGen>
<DependentUpon>Common.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Controllers\TokenController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>TokenController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Controllers\UserController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserController.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Models\Validation\Validator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Validator.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\Exception.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Exception.resx</DependentUpon>
+ </Compile>
</ItemGroup>
<ItemGroup>
@@ -48,7 +68,8 @@
</EmbeddedResource>
<EmbeddedResource Update="Resources\Controllers\TokenController.resx">
<SubType>Designer</SubType>
- <Generator></Generator>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TokenController.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Controllers\TokenController.zh.resx">
<SubType>Designer</SubType>
@@ -58,7 +79,16 @@
<Generator></Generator>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Controllers\UserController.resx">
- <Generator></Generator>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\Validator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Validator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\Exception.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Exception.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>