diff options
author | 杨宇千 <crupest@outlook.com> | 2019-10-24 20:15:58 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-24 20:15:58 +0800 |
commit | 7305358a88ffc87f51f7b78deb4f07ef99120beb (patch) | |
tree | 7ca5010a06829cc5fadea1ea17ae72d082fc344c | |
parent | 297d0c9029360f1d5334ed843b9b299356740ec1 (diff) | |
parent | a0f3cd7599a48c14fb5492fb1c6e2dbd0a82fb45 (diff) | |
download | timeline-7305358a88ffc87f51f7b78deb4f07ef99120beb.tar.gz timeline-7305358a88ffc87f51f7b78deb4f07ef99120beb.tar.bz2 timeline-7305358a88ffc87f51f7b78deb4f07ef99120beb.zip |
Merge pull request #50 from crupest/refactor
Refactor : A Huge Step
115 files changed, 7184 insertions, 3012 deletions
diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs deleted file mode 100644 index 4751e95f..00000000 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ /dev/null @@ -1,76 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using System;
-using System.Net;
-using System.Threading.Tasks;
-using Timeline.Tests.Helpers;
-using Timeline.Tests.Helpers.Authentication;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests
-{
- public class AuthorizationUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private const string AuthorizeUrl = "Test/User/Authorize";
- private const string UserUrl = "Test/User/User";
- private const string AdminUrl = "Test/User/Admin";
-
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public AuthorizationUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async Task UnauthenticationTest()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.GetAsync(AuthorizeUrl);
- response.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
- }
- }
-
- [Fact]
- public async Task AuthenticationTest()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- var response = await client.GetAsync(AuthorizeUrl);
- response.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- }
-
- [Fact]
- public async Task UserAuthorizationTest()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- var response1 = await client.GetAsync(UserUrl);
- response1.Should().HaveStatusCode(HttpStatusCode.OK);
- var response2 = await client.GetAsync(AdminUrl);
- response2.Should().HaveStatusCode(HttpStatusCode.Forbidden);
- }
- }
-
- [Fact]
- public async Task AdminAuthorizationTest()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var response1 = await client.GetAsync(UserUrl);
- response1.Should().HaveStatusCode(HttpStatusCode.OK);
- var response2 = await client.GetAsync(AdminUrl);
- response2.Should().HaveStatusCode(HttpStatusCode.OK);
- }
- }
- }
-}
diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs new file mode 100644 index 00000000..53b6c606 --- /dev/null +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -0,0 +1,122 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Timeline.Controllers;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Mock.Data;
+using Timeline.Tests.Mock.Services;
+using Xunit;
+using static Timeline.ErrorCodes.Http.Token;
+
+namespace Timeline.Tests.Controllers
+{
+ public class TokenControllerTest : IDisposable
+ {
+ private readonly Mock<IUserService> _mockUserService = new Mock<IUserService>();
+ private readonly TestClock _mockClock = new TestClock();
+
+
+ private readonly TokenController _controller;
+
+ public TokenControllerTest()
+ {
+ _controller = new TokenController(_mockUserService.Object,
+ NullLogger<TokenController>.Instance, _mockClock,
+ TestStringLocalizerFactory.Create().Create<TokenController>());
+ }
+
+ public void Dispose()
+ {
+ _controller.Dispose();
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData(100)]
+ public async Task Create_Ok(int? expire)
+ {
+ var mockCurrentTime = DateTime.Now;
+ _mockClock.MockCurrentTime = mockCurrentTime;
+ var createResult = new CreateTokenResult
+ {
+ Token = "mocktokenaaaaa",
+ User = MockUser.User.Info
+ };
+ _mockUserService.Setup(s => s.CreateToken("u", "p", expire == null ? null : (DateTime?)mockCurrentTime.AddDays(expire.Value))).ReturnsAsync(createResult);
+ var action = await _controller.Create(new CreateTokenRequest
+ {
+ Username = "u",
+ Password = "p",
+ Expire = expire
+ });
+ action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeEquivalentTo(createResult);
+ }
+
+ [Fact]
+ public async Task Create_UserNotExist()
+ {
+ _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new UserNotExistException("u"));
+ var action = await _controller.Create(new CreateTokenRequest
+ {
+ Username = "u",
+ Password = "p",
+ Expire = null
+ });
+ action.Result.Should().BeAssignableTo<BadRequestObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(Create.BadCredential);
+ }
+
+ [Fact]
+ public async Task Create_BadPassword()
+ {
+ _mockUserService.Setup(s => s.CreateToken("u", "p", null)).ThrowsAsync(new BadPasswordException("u"));
+ var action = await _controller.Create(new CreateTokenRequest
+ {
+ Username = "u",
+ Password = "p",
+ Expire = null
+ });
+ action.Result.Should().BeAssignableTo<BadRequestObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(Create.BadCredential);
+ }
+
+ [Fact]
+ public async Task Verify_Ok()
+ {
+ const string token = "aaaaaaaaaaaaaa";
+ _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(MockUser.User.Info);
+ var action = await _controller.Verify(new VerifyTokenRequest { Token = token });
+ action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeAssignableTo<VerifyTokenResponse>()
+ .Which.User.Should().BeEquivalentTo(MockUser.User.Info);
+ }
+
+ public static IEnumerable<object[]> Verify_BadRequest_Data()
+ {
+ 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 };
+ }
+
+ [Theory]
+ [MemberData(nameof(Verify_BadRequest_Data))]
+ public async Task Verify_BadRequest(Exception e, int code)
+ {
+ const string token = "aaaaaaaaaaaaaa";
+ _mockUserService.Setup(s => s.VerifyToken(token)).ThrowsAsync(e);
+ var action = await _controller.Verify(new VerifyTokenRequest { Token = token });
+ action.Result.Should().BeAssignableTo<BadRequestObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(code);
+ }
+ }
+}
diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs new file mode 100644 index 00000000..781ec111 --- /dev/null +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -0,0 +1,222 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Timeline.Controllers;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Mock.Data;
+using Timeline.Tests.Mock.Services;
+using Xunit;
+using static Timeline.ErrorCodes.Http.User;
+
+namespace Timeline.Tests.Controllers
+{
+ public class UserControllerTest : IDisposable
+ {
+ private readonly Mock<IUserService> _mockUserService = new Mock<IUserService>();
+
+ private readonly UserController _controller;
+
+ public UserControllerTest()
+ {
+ _controller = new UserController(NullLogger<UserController>.Instance,
+ _mockUserService.Object,
+ TestStringLocalizerFactory.Create());
+ }
+
+ public void Dispose()
+ {
+ _controller.Dispose();
+ }
+
+ [Fact]
+ public async Task GetList_Success()
+ {
+ var array = MockUser.UserInfoList.ToArray();
+ _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(array);
+ var action = await _controller.List();
+ action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeEquivalentTo(array);
+ }
+
+ [Fact]
+ public async Task Get_Success()
+ {
+ const string username = "aaa";
+ _mockUserService.Setup(s => s.GetUser(username)).ReturnsAsync(MockUser.User.Info);
+ var action = await _controller.Get(username);
+ action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeEquivalentTo(MockUser.User.Info);
+ }
+
+ [Fact]
+ public async Task Get_NotFound()
+ {
+ const string username = "aaa";
+ _mockUserService.Setup(s => s.GetUser(username)).Returns(Task.FromResult<UserInfo>(null));
+ var action = await _controller.Get(username);
+ action.Result.Should().BeAssignableTo<NotFoundObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(Get.NotExist);
+ }
+
+ [Theory]
+ [InlineData(PutResult.Create, true)]
+ [InlineData(PutResult.Modify, false)]
+ public async Task Put_Success(PutResult result, bool create)
+ {
+ const string username = "aaa";
+ const string password = "ppp";
+ const bool administrator = true;
+ _mockUserService.Setup(s => s.PutUser(username, password, administrator)).ReturnsAsync(result);
+ var action = await _controller.Put(new UserPutRequest
+ {
+ Password = password,
+ Administrator = administrator
+ }, username);
+ var response = action.Result.Should().BeAssignableTo<ObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonPutResponse>()
+ .Which;
+ response.Code.Should().Be(0);
+ response.Data.Create.Should().Be(create);
+ }
+
+ [Fact]
+ public async Task Patch_Success()
+ {
+ const string username = "aaa";
+ const string password = "ppp";
+ const bool administrator = true;
+ _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).Returns(Task.CompletedTask);
+ var action = await _controller.Patch(new UserPatchRequest
+ {
+ Password = password,
+ Administrator = administrator
+ }, username);
+ action.Should().BeAssignableTo<OkResult>();
+ }
+
+ [Fact]
+ public async Task Patch_NotExist()
+ {
+ const string username = "aaa";
+ const string password = "ppp";
+ const bool administrator = true;
+ _mockUserService.Setup(s => s.PatchUser(username, password, administrator)).ThrowsAsync(new UserNotExistException());
+ var action = await _controller.Patch(new UserPatchRequest
+ {
+ Password = password,
+ Administrator = administrator
+ }, username);
+ action.Should().BeAssignableTo<NotFoundObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(Patch.NotExist);
+ }
+
+ [Fact]
+ public async Task Delete_Delete()
+ {
+ const string username = "aaa";
+ _mockUserService.Setup(s => s.DeleteUser(username)).Returns(Task.CompletedTask);
+ var action = await _controller.Delete(username);
+ var body = action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonDeleteResponse>()
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Delete.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task Delete_NotExist()
+ {
+ const string username = "aaa";
+ _mockUserService.Setup(s => s.DeleteUser(username)).ThrowsAsync(new UserNotExistException());
+ var action = await _controller.Delete(username);
+ var body = action.Result.Should().BeAssignableTo<OkObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonDeleteResponse>()
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Delete.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task Op_ChangeUsername_Success()
+ {
+ const string oldUsername = "aaa";
+ const string newUsername = "bbb";
+ _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).Returns(Task.CompletedTask);
+ var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername });
+ action.Should().BeAssignableTo<OkResult>();
+ }
+
+ [Theory]
+ [InlineData(typeof(UserNotExistException), Op.ChangeUsername.NotExist)]
+ [InlineData(typeof(UsernameConfictException), Op.ChangeUsername.AlreadyExist)]
+ public async Task Op_ChangeUsername_Failure(Type exceptionType, int code)
+ {
+ const string oldUsername = "aaa";
+ const string newUsername = "bbb";
+ _mockUserService.Setup(s => s.ChangeUsername(oldUsername, newUsername)).ThrowsAsync(Activator.CreateInstance(exceptionType) as Exception);
+ var action = await _controller.ChangeUsername(new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername });
+ action.Should().BeAssignableTo<BadRequestObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(code);
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_Success()
+ {
+ const string username = "aaa";
+ const string oldPassword = "aaa";
+ const string newPassword = "bbb";
+ _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).Returns(Task.CompletedTask);
+
+ _controller.ControllerContext = new ControllerContext()
+ {
+ HttpContext = new DefaultHttpContext()
+ {
+ User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
+ {
+ new Claim(ClaimTypes.Name, username)
+ }, "TestAuthType"))
+ }
+ };
+
+ var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
+ action.Should().BeAssignableTo<OkResult>();
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_BadPassword()
+ {
+ const string username = "aaa";
+ const string oldPassword = "aaa";
+ const string newPassword = "bbb";
+ _mockUserService.Setup(s => s.ChangePassword(username, oldPassword, newPassword)).ThrowsAsync(new BadPasswordException());
+
+ _controller.ControllerContext = new ControllerContext()
+ {
+ HttpContext = new DefaultHttpContext()
+ {
+ User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
+ {
+ new Claim(ClaimTypes.Name, username)
+ }, "TestAuthType"))
+ }
+ };
+
+ var action = await _controller.ChangePassword(new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
+ action.Should().BeAssignableTo<BadRequestObjectResult>()
+ .Which.Value.Should().BeAssignableTo<CommonResponse>()
+ .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword);
+ }
+ }
+}
diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index f75ab71b..b5681491 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -26,27 +26,21 @@ namespace Timeline.Tests [Fact]
public void DeleteUserShouldAlsoDeleteAvatar()
{
- _context.UserAvatars.Count().Should().Be(2);
var user = _context.Users.First();
- _context.Users.Remove(user);
- _context.SaveChanges();
- _context.UserAvatars.Count().Should().Be(1);
- }
-
- [Fact]
- public void DeleteUserShouldAlsoDeleteDetail()
- {
- var user = _context.Users.First();
- _context.UserDetails.Add(new UserDetailEntity
+ _context.UserAvatars.Count().Should().Be(0);
+ _context.UserAvatars.Add(new UserAvatar
{
+ Data = null,
+ Type = null,
+ ETag = null,
+ LastModified = DateTime.Now,
UserId = user.Id
});
_context.SaveChanges();
- _context.UserDetails.Count().Should().Be(1);
-
+ _context.UserAvatars.Count().Should().Be(1);
_context.Users.Remove(user);
_context.SaveChanges();
- _context.UserDetails.Count().Should().Be(0);
+ _context.UserAvatars.Count().Should().Be(0);
}
}
}
diff --git a/Timeline.Tests/GlobalSuppressions.cs b/Timeline.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..1d1d294b --- /dev/null +++ b/Timeline.Tests/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs index e67a172a..08f10b2b 100644 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using System;
using System.Net;
using System.Net.Http;
+using System.Text;
using Timeline.Models.Http;
namespace Timeline.Tests.Helpers
@@ -23,8 +24,25 @@ namespace Timeline.Tests.Helpers string padding = new string('\t', context.Depth);
var res = (HttpResponseMessage)value;
- var body = res.Content.ReadAsStringAsync().Result;
- return $"{newline}{padding} Status Code: {res.StatusCode} ; Body: {body.Substring(0, Math.Min(body.Length, 20))} ;";
+
+ var builder = new StringBuilder();
+ builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: ");
+
+ try
+ {
+ var body = res.Content.ReadAsStringAsync().Result;
+ if (body.Length > 40)
+ {
+ body = body[0..40] + " ...";
+ }
+ builder.Append(body);
+ }
+ catch (AggregateException)
+ {
+ builder.Append("NOT A STRING.");
+ }
+
+ return builder.ToString();
}
}
@@ -43,15 +61,20 @@ namespace Timeline.Tests.Helpers protected override string Identifier => "HttpResponseMessage";
+ public AndConstraint<HttpResponseMessage> HaveStatusCode(int expected, string because = "", params object[] becauseArgs)
+ {
+ return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs);
+ }
+
public AndConstraint<HttpResponseMessage> HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs)
{
Execute.Assertion.BecauseOf(because, becauseArgs)
.ForCondition(Subject.StatusCode == expected)
- .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.\nResponse is {2}.", expected, Subject.StatusCode, Subject);
+ .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode);
return new AndConstraint<HttpResponseMessage>(Subject);
}
- public AndWhichConstraint<HttpResponseMessage, T> HaveBodyAsJson<T>(string because = "", params object[] becauseArgs)
+ public AndWhichConstraint<HttpResponseMessage, T> HaveJsonBody<T>(string because = "", params object[] becauseArgs)
{
var a = Execute.Assertion.BecauseOf(because, becauseArgs);
string body;
@@ -59,22 +82,14 @@ namespace Timeline.Tests.Helpers {
body = Subject.Content.ReadAsStringAsync().Result;
}
- catch (Exception e)
+ catch (AggregateException e)
{
- a.FailWith("Failed to read response body of {context:HttpResponseMessage}{reason}.\nException is {0}.", e);
+ a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions);
return new AndWhichConstraint<HttpResponseMessage, T>(Subject, null);
}
- try
- {
- var result = JsonConvert.DeserializeObject<T>(body);
- return new AndWhichConstraint<HttpResponseMessage, T>(Subject, result);
- }
- catch (Exception e)
- {
- a.FailWith("Failed to convert response body of {context:HttpResponseMessage} to {0}{reason}.\nResponse is {1}.\nException is {2}.", typeof(T).FullName, Subject, e);
- return new AndWhichConstraint<HttpResponseMessage, T>(Subject, null);
- }
+ var result = JsonConvert.DeserializeObject<T>(body);
+ return new AndWhichConstraint<HttpResponseMessage, T>(Subject, result);
}
}
@@ -85,54 +100,42 @@ namespace Timeline.Tests.Helpers return new HttpResponseMessageAssertions(instance);
}
- public static AndConstraint<HttpResponseMessage> HaveStatusCodeOk(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
- {
- return assertions.HaveStatusCode(HttpStatusCode.OK, because, becauseArgs);
- }
-
- public static AndConstraint<HttpResponseMessage> HaveStatusCodeCreated(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
- {
- return assertions.HaveStatusCode(HttpStatusCode.Created, because, becauseArgs);
- }
-
- public static AndConstraint<HttpResponseMessage> HaveStatusCodeBadRequest(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
- {
- return assertions.HaveStatusCode(HttpStatusCode.BadRequest, because, becauseArgs);
- }
-
- public static AndConstraint<HttpResponseMessage> HaveStatusCodeNotFound(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
- {
- return assertions.HaveStatusCode(HttpStatusCode.NotFound, because, becauseArgs);
- }
-
- public static AndWhichConstraint<HttpResponseMessage, CommonResponse> HaveBodyAsCommonResponse(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
- {
- return assertions.HaveBodyAsJson<CommonResponse>(because, becauseArgs);
- }
-
- public static void HaveBodyAsCommonResponseWithCode(this HttpResponseMessageAssertions assertions, int expected, string because = "", params object[] becauseArgs)
+ public static AndWhichConstraint<HttpResponseMessage, CommonResponse> HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
{
- assertions.HaveBodyAsCommonResponse(because, becauseArgs).Which.Code.Should().Be(expected, because, becauseArgs);
+ return assertions.HaveJsonBody<CommonResponse>(because, becauseArgs);
}
- public static void BePutCreated(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ public static AndWhichConstraint<HttpResponseMessage, CommonDataResponse<TData>> HaveCommonDataBody<TData>(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
{
- assertions.HaveStatusCodeCreated(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Created, because, becauseArgs);
+ return assertions.HaveJsonBody<CommonDataResponse<TData>>(because, becauseArgs);
}
- public static void BePutModified(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs)
{
- assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonPutResponse.Modified, because, becauseArgs);
+ var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs)
+ .And.Should().HaveJsonBody<CommonPutResponse>(because, becauseArgs)
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Create.Should().Be(create);
}
- public static void BeDeleteDeleted(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs)
{
- assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.Deleted, because, becauseArgs);
+ var body = assertions.HaveStatusCode(200, because, becauseArgs)
+ .And.Should().HaveJsonBody<CommonDeleteResponse>(because, becauseArgs)
+ .Which;
+ body.Code.Should().Be(0);
+ body.Data.Delete.Should().Be(delete);
}
- public static void BeDeleteNotExist(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null)
{
- assertions.HaveStatusCodeOk(because, becauseArgs).And.Should().HaveBodyAsCommonResponse(because, becauseArgs).Which.Should().BeEquivalentTo(CommonDeleteResponse.NotExists, because, becauseArgs);
+ message = string.IsNullOrEmpty(message) ? "" : ", " + message;
+ assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message)
+ .And.Should().HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message)
+ .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel,
+ "Invalid Model Error must have code {0} in body{1}",
+ ErrorCodes.Http.Common.InvalidModel, message);
}
}
}
diff --git a/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs b/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs new file mode 100644 index 00000000..b78309c0 --- /dev/null +++ b/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions;
+using FluentAssertions.Primitives;
+using FluentAssertions.Specialized;
+using System;
+using System.Threading.Tasks;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class AsyncFunctionAssertionsExtensions
+ {
+ public static async Task<AndConstraint<ObjectAssertions>> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs)
+ {
+ return (await assertions.ThrowAsync<Exception>(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType);
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index c8a79e58..34d7e460 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -13,8 +13,8 @@ namespace Timeline.Tests.Helpers.Authentication public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, int? expireOffset = null)
{
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, ExpireOffset = expireOffset });
- response.Should().HaveStatusCodeOk();
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset });
+ response.Should().HaveStatusCode(200);
var result = JsonConvert.DeserializeObject<CreateTokenResponse>(await response.Content.ReadAsStringAsync());
return result;
}
@@ -29,12 +29,12 @@ namespace Timeline.Tests.Helpers.Authentication public static Task<HttpClient> CreateClientAsUser<T>(this WebApplicationFactory<T> factory) where T : class
{
- return factory.CreateClientWithCredential(MockUsers.UserUsername, MockUsers.UserPassword);
+ return factory.CreateClientWithCredential(MockUser.User.Username, MockUser.User.Password);
}
public static Task<HttpClient> CreateClientAsAdmin<T>(this WebApplicationFactory<T> factory) where T : class
{
- return factory.CreateClientWithCredential(MockUsers.AdminUsername, MockUsers.AdminPassword);
+ return factory.CreateClientWithCredential(MockUser.Admin.Username, MockUser.Admin.Password);
}
}
}
diff --git a/Timeline.Tests/Helpers/HttpClientExtensions.cs b/Timeline.Tests/Helpers/HttpClientExtensions.cs index b9204fcc..38641f90 100644 --- a/Timeline.Tests/Helpers/HttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json;
+using System;
using System.Net.Http;
using System.Net.Http.Headers;
+using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
@@ -10,11 +12,24 @@ namespace Timeline.Tests.Helpers {
public static Task<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient client, string url, T body)
{
- return client.PatchAsync(url, new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"));
+ return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ public static Task<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient client, Uri url, T body)
+ {
+ return client.PatchAsync(url, new StringContent(
+ JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json));
}
public static Task<HttpResponseMessage> PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType)
{
+ return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
+ public static Task<HttpResponseMessage> PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType)
+ {
var content = new ByteArrayContent(body);
content.Headers.ContentLength = body.Length;
content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
diff --git a/Timeline.Tests/Helpers/ImageHelper.cs b/Timeline.Tests/Helpers/ImageHelper.cs index 2a2f3870..9bed0917 100644 --- a/Timeline.Tests/Helpers/ImageHelper.cs +++ b/Timeline.Tests/Helpers/ImageHelper.cs @@ -9,26 +9,18 @@ namespace Timeline.Tests.Helpers {
public static byte[] CreatePngWithSize(int width, int height)
{
- using (var image = new Image<Rgba32>(width, height))
- {
- using (var stream = new MemoryStream())
- {
- image.SaveAsPng(stream);
- return stream.ToArray();
- }
- }
+ using var image = new Image<Rgba32>(width, height);
+ using var stream = new MemoryStream();
+ image.SaveAsPng(stream);
+ return stream.ToArray();
}
public static byte[] CreateImageWithSize(int width, int height, IImageFormat format)
{
- using (var image = new Image<Rgba32>(width, height))
- {
- using (var stream = new MemoryStream())
- {
- image.Save(stream, format);
- return stream.ToArray();
- }
- }
+ using var image = new Image<Rgba32>(width, height);
+ using var stream = new MemoryStream();
+ image.Save(stream, format);
+ return stream.ToArray();
}
}
}
diff --git a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs b/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs deleted file mode 100644 index af432095..00000000 --- a/Timeline.Tests/Helpers/InvalidModelTestHelpers.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Net.Http;
-using System.Threading.Tasks;
-using Timeline.Models.Http;
-
-namespace Timeline.Tests.Helpers
-{
- public static class InvalidModelTestHelpers
- {
- public static async Task TestPostInvalidModel<T>(HttpClient client, string url, T body)
- {
- var response = await client.PostAsJsonAsync(url, body);
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.InvalidModel);
- }
-
- public static async Task TestPutInvalidModel<T>(HttpClient client, string url, T body)
- {
- var response = await client.PutAsJsonAsync(url, body);
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.InvalidModel);
- }
- }
-}
diff --git a/Timeline.Tests/Helpers/MyTestLoggerFactory.cs b/Timeline.Tests/Helpers/MyTestLoggerFactory.cs deleted file mode 100644 index b9960378..00000000 --- a/Timeline.Tests/Helpers/MyTestLoggerFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests.Helpers
-{
- public static class Logging
- {
- public static ILoggerFactory Create(ITestOutputHelper outputHelper)
- {
- // TODO: Use test output.
- return NullLoggerFactory.Instance;
- }
-
- public static IWebHostBuilder ConfigureTestLogging(this IWebHostBuilder builder)
- {
- builder.ConfigureLogging(logging =>
- {
- //logging.AddXunit(outputHelper);
- });
- return builder;
- }
- }
-}
diff --git a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs b/Timeline.Tests/Helpers/MyWebApplicationFactory.cs deleted file mode 100644 index dfbe6620..00000000 --- a/Timeline.Tests/Helpers/MyWebApplicationFactory.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.AspNetCore.TestHost;
-using Microsoft.Data.Sqlite;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Diagnostics;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using System;
-using Timeline.Entities;
-using Timeline.Services;
-using Timeline.Tests.Mock.Data;
-using Timeline.Tests.Mock.Services;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests.Helpers
-{
- public class MyWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
- {
- protected override void ConfigureWebHost(IWebHostBuilder builder)
- {
- builder.ConfigureTestServices(services =>
- {
- services.AddSingleton<IClock, TestClock>();
- });
- }
- }
-
- public static class WebApplicationFactoryExtensions
- {
- public static WebApplicationFactory<TEntry> WithTestConfig<TEntry>(this WebApplicationFactory<TEntry> factory, ITestOutputHelper outputHelper, out Action disposeAction) where TEntry : class
- {
- // We should keep the connection, so the database is persisted but not recreate every time.
- // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests .
- SqliteConnection _databaseConnection = new SqliteConnection("Data Source=:memory:;");
- _databaseConnection.Open();
-
- {
- var options = new DbContextOptionsBuilder<DatabaseContext>()
- .UseSqlite(_databaseConnection)
- .ConfigureWarnings(builder =>
- {
- builder.Throw(RelationalEventId.QueryClientEvaluationWarning);
- })
- .Options;
-
- using (var context = new DatabaseContext(options))
- {
- TestDatabase.InitDatabase(context);
- };
- }
-
- disposeAction = () =>
- {
- _databaseConnection.Close();
- _databaseConnection.Dispose();
- };
-
- return factory.WithWebHostBuilder(builder =>
- {
- builder.ConfigureTestLogging()
- .ConfigureServices(services =>
- {
- services.AddEntityFrameworkSqlite();
- services.AddDbContext<DatabaseContext>(options =>
- {
- options.UseSqlite(_databaseConnection);
- });
- });
- });
- }
- }
-}
diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs new file mode 100644 index 00000000..b0187a30 --- /dev/null +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using Timeline.Entities;
+using Timeline.Tests.Mock.Data;
+
+namespace Timeline.Tests.Helpers
+{
+ public class TestApplication : IDisposable
+ {
+ public SqliteConnection DatabaseConnection { get; } = new SqliteConnection("Data Source=:memory:;");
+ public WebApplicationFactory<Startup> Factory { get; }
+
+ public TestApplication(WebApplicationFactory<Startup> factory)
+ {
+ // We should keep the connection, so the database is persisted but not recreate every time.
+ // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests .
+ DatabaseConnection.Open();
+
+ {
+ var options = new DbContextOptionsBuilder<DatabaseContext>()
+ .UseSqlite(DatabaseConnection)
+ .Options;
+
+ using (var context = new DatabaseContext(options))
+ {
+ TestDatabase.InitDatabase(context);
+ };
+ }
+
+ Factory = factory.WithWebHostBuilder(builder =>
+ {
+ builder.ConfigureServices(services =>
+ {
+ services.AddEntityFrameworkSqlite();
+ services.AddDbContext<DatabaseContext>(options =>
+ {
+ options.UseSqlite(DatabaseConnection);
+ });
+ });
+ });
+ }
+
+ public void Dispose()
+ {
+ DatabaseConnection.Close();
+ DatabaseConnection.Dispose();
+ }
+ }
+}
diff --git a/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..a31d98f5 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,69 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class AuthorizationTest : IClassFixture<WebApplicationFactory<Startup>>, IDisposable
+ {
+ private readonly TestApplication _testApp;
+ private readonly WebApplicationFactory<Startup> _factory;
+
+ public AuthorizationTest(WebApplicationFactory<Startup> factory)
+ {
+ _testApp = new TestApplication(factory);
+ _factory = _testApp.Factory;
+ }
+
+ public void Dispose()
+ {
+ _testApp.Dispose();
+ }
+
+ private const string BaseUrl = "testing/auth/";
+ private const string AuthorizeUrl = BaseUrl + "Authorize";
+ private const string UserUrl = BaseUrl + "User";
+ private const string AdminUrl = BaseUrl + "Admin";
+
+ [Fact]
+ public async Task UnauthenticationTest()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var response = await client.GetAsync(AuthorizeUrl);
+ response.Should().HaveStatusCode(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task AuthenticationTest()
+ {
+ using var client = await _factory.CreateClientAsUser();
+ var response = await client.GetAsync(AuthorizeUrl);
+ response.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task UserAuthorizationTest()
+ {
+ using var client = await _factory.CreateClientAsUser();
+ var response1 = await client.GetAsync(UserUrl);
+ response1.Should().HaveStatusCode(HttpStatusCode.OK);
+ var response2 = await client.GetAsync(AdminUrl);
+ response2.Should().HaveStatusCode(HttpStatusCode.Forbidden);
+ }
+
+ [Fact]
+ public async Task AdminAuthorizationTest()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var response1 = await client.GetAsync(UserUrl);
+ response1.Should().HaveStatusCode(HttpStatusCode.OK);
+ var response2 = await client.GetAsync(AdminUrl);
+ response2.Should().HaveStatusCode(HttpStatusCode.OK);
+ }
+ }
+}
diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..e9b6e1e9 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,176 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Timeline.Tests.Mock.Data;
+using Xunit;
+using static Timeline.ErrorCodes.Http.Token;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class TokenTest : IClassFixture<WebApplicationFactory<Startup>>, IDisposable
+ {
+ private const string CreateTokenUrl = "token/create";
+ private const string VerifyTokenUrl = "token/verify";
+
+ private readonly TestApplication _testApp;
+ private readonly WebApplicationFactory<Startup> _factory;
+
+ public TokenTest(WebApplicationFactory<Startup> factory)
+ {
+ _testApp = new TestApplication(factory);
+ _factory = _testApp.Factory;
+ }
+
+ public void Dispose()
+ {
+ _testApp.Dispose();
+ }
+
+ public static IEnumerable<object[]> CreateToken_InvalidModel_Data()
+ {
+ yield return new[] { null, "p", null };
+ yield return new[] { "u", null, null };
+ yield return new object[] { "u", "p", 2000 };
+ yield return new object[] { "u", "p", -1 };
+ }
+
+ [Theory]
+ [MemberData(nameof(CreateToken_InvalidModel_Data))]
+ public async Task CreateToken_InvalidModel(string username, string password, int expire)
+ {
+ using var client = _factory.CreateDefaultClient();
+ (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest
+ {
+ Username = username,
+ Password = password,
+ Expire = expire
+ })).Should().BeInvalidModel();
+ }
+
+ public static IEnumerable<object[]> CreateToken_UserCredential_Data()
+ {
+ yield return new[] { "usernotexist", "p" };
+ yield return new[] { MockUser.User.Username, "???" };
+ }
+
+ [Theory]
+ [MemberData(nameof(CreateToken_UserCredential_Data))]
+ public async void CreateToken_UserCredential(string username, string password)
+ {
+ using var client = _factory.CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(CreateTokenUrl,
+ new CreateTokenRequest { Username = username, Password = password });
+ response.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Create.BadCredential);
+ }
+
+ [Fact]
+ public async Task CreateToken_Success()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(CreateTokenUrl,
+ new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password });
+ var body = response.Should().HaveStatusCode(200)
+ .And.Should().HaveJsonBody<CreateTokenResponse>().Which;
+ body.Token.Should().NotBeNullOrWhiteSpace();
+ body.User.Should().BeEquivalentTo(MockUser.User.Info);
+ }
+
+ [Fact]
+ public async Task VerifyToken_InvalidModel()
+ {
+ using var client = _factory.CreateDefaultClient();
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = null })).Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task VerifyToken_BadFormat()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = "bad token hahaha" });
+ response.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Verify.BadFormat);
+ }
+
+ [Fact]
+ public async Task VerifyToken_OldVersion()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token;
+
+ using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
+ {
+ // create a user for test
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ await userService.PatchUser(MockUser.User.Username, null, null);
+ }
+
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = token }))
+ .Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Verify.OldVersion);
+ }
+
+ [Fact]
+ public async Task VerifyToken_UserNotExist()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var token = (await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password)).Token;
+
+ using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
+ {
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ await userService.DeleteUser(MockUser.User.Username);
+ }
+
+ (await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = token }))
+ .Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Verify.UserNotExist);
+ }
+
+ //[Fact]
+ //public async Task VerifyToken_Expired()
+ //{
+ // using (var client = _factory.CreateDefaultClient())
+ // {
+ // // I can only control the token expired time but not current time
+ // // because verify logic is encapsuled in other library.
+ // var mockClock = _factory.GetTestClock();
+ // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2);
+ // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token;
+ // var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ // new VerifyTokenRequest { Token = token });
+ // response.Should().HaveStatusCodeBadRequest()
+ // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired);
+ // mockClock.MockCurrentTime = null;
+ // }
+ //}
+
+ [Fact]
+ public async Task VerifyToken_Success()
+ {
+ using var client = _factory.CreateDefaultClient();
+ var createTokenResult = await client.CreateUserTokenAsync(MockUser.User.Username, MockUser.User.Password);
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl,
+ new VerifyTokenRequest { Token = createTokenResult.Token });
+ response.Should().HaveStatusCode(200)
+ .And.Should().HaveJsonBody<VerifyTokenResponse>()
+ .Which.User.Should().BeEquivalentTo(MockUser.User.Info);
+ }
+ }
+}
diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 2a3442d1..ce389046 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -3,9 +3,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
-using SixLabors.ImageSharp.Formats.Gif;
using System;
using System.Collections.Generic;
using System.IO;
@@ -14,31 +14,33 @@ using System.Net.Http; using System.Net.Http.Headers;
using System.Threading.Tasks;
using Timeline.Controllers;
-using Timeline.Models.Http;
using Timeline.Services;
using Timeline.Tests.Helpers;
using Timeline.Tests.Helpers.Authentication;
using Xunit;
-using Xunit.Abstractions;
+using static Timeline.ErrorCodes.Http.Common;
+using static Timeline.ErrorCodes.Http.UserAvatar;
namespace Timeline.Tests.IntegratedTests
{
- public class UserAvatarUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
+ public class UserAvatarUnitTest : IClassFixture<WebApplicationFactory<Startup>>, IDisposable
{
+ private readonly TestApplication _testApp;
private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
- public UserAvatarUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
+ public UserAvatarUnitTest(WebApplicationFactory<Startup> factory)
{
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
+ _testApp = new TestApplication(factory);
+ _factory = _testApp.Factory;
}
public void Dispose()
{
- _disposeAction();
+ _testApp.Dispose();
}
[Fact]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "HttpMessageRequest should be disposed ???")]
public async Task Test()
{
Avatar mockAvatar = new Avatar
@@ -51,8 +53,9 @@ namespace Timeline.Tests.IntegratedTests {
{
var res = await client.GetAsync("users/usernotexist/avatar");
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Get_UserNotExist);
+ res.Should().HaveStatusCode(404)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Get.UserNotExist);
}
var env = _factory.Server.Host.Services.GetRequiredService<IWebHostEnvironment>();
@@ -61,7 +64,7 @@ namespace Timeline.Tests.IntegratedTests async Task GetReturnDefault(string username = "user")
{
var res = await client.GetAsync($"users/{username}/avatar");
- res.Should().HaveStatusCodeOk();
+ res.Should().HaveStatusCode(200);
res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
var body = await res.Content.ReadAsByteArrayAsync();
body.Should().Equal(defaultAvatarData);
@@ -70,7 +73,7 @@ namespace Timeline.Tests.IntegratedTests EntityTagHeaderValue eTag;
{
var res = await client.GetAsync($"users/user/avatar");
- res.Should().HaveStatusCodeOk();
+ res.Should().HaveStatusCode(200);
res.Content.Headers.ContentType.MediaType.Should().Be("image/png");
var body = await res.Content.ReadAsByteArrayAsync();
body.Should().Equal(defaultAvatarData);
@@ -92,7 +95,7 @@ namespace Timeline.Tests.IntegratedTests request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd");
var res = await client.SendAsync(request);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_BadFormat_IfNonMatch);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch);
}
{
@@ -122,7 +125,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Missing_ContentLength);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentLength);
}
{
@@ -130,7 +133,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentLength = 1;
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Missing_ContentType);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentType);
}
{
@@ -139,7 +142,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_Zero_ContentLength);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Zero_ContentLength);
}
{
@@ -153,7 +156,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_TooBig);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.TooBig);
}
{
@@ -162,7 +165,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Less);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller);
}
{
@@ -171,25 +174,25 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
var res = await client.PutAsync("users/user/avatar", content);
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Content_UnmatchedLength_Bigger);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_CantDecode);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_UnmatchedFormat);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat);
}
{
var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png");
res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_BadFormat_BadSize);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize);
}
{
@@ -197,7 +200,7 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCode(HttpStatusCode.OK);
var res2 = await client.GetAsync("users/user/avatar");
- res2.Should().HaveStatusCodeOk();
+ res2.Should().HaveStatusCode(200);
res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type);
var body = await res2.Content.ReadAsByteArrayAsync();
body.Should().Equal(mockAvatar.Data);
@@ -219,19 +222,19 @@ namespace Timeline.Tests.IntegratedTests {
var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png");
res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_Forbid);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.Forbid);
}
{
var res = await client.DeleteAsync("users/admin/avatar");
res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Delete_Forbid);
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.Forbid);
}
for (int i = 0; i < 2; i++) // double delete should work.
{
var res = await client.DeleteAsync("users/user/avatar");
- res.Should().HaveStatusCodeOk();
+ res.Should().HaveStatusCode(200);
await GetReturnDefault();
}
}
@@ -251,14 +254,34 @@ namespace Timeline.Tests.IntegratedTests {
var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png");
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Put_UserNotExist);
+ res.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Put.UserNotExist);
}
{
var res = await client.DeleteAsync("users/usernotexist/avatar");
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserAvatarController.ErrorCodes.Delete_UserNotExist);
+ res.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist);
+ }
+ }
+
+ // bad username check
+ using (var client = await _factory.CreateClientAsAdmin())
+ {
+ {
+ var res = await client.GetAsync("users/u!ser/avatar");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png");
+ res.Should().BeInvalidModel();
+ }
+
+ {
+ var res = await client.DeleteAsync("users/u!ser/avatar");
+ res.Should().BeInvalidModel();
}
}
}
diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs deleted file mode 100644 index ba15b7ca..00000000 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ /dev/null @@ -1,146 +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.Models.Http;
-using Timeline.Tests.Helpers;
-using Timeline.Tests.Helpers.Authentication;
-using Timeline.Tests.Mock.Data;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests.IntegratedTests
-{
- public class UserDetailTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public UserDetailTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async Task TestAsUser()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- {
- var res = await client.GetAsync($"users/usernotexist/nickname");
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.GetNickname_UserNotExist);
- }
-
- {
- var res = await client.GetAsync($"users/usernotexist/details");
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Get_UserNotExist);
- }
-
- async Task GetAndTest(UserDetail d)
- {
- var res = await client.GetAsync($"users/{MockUsers.UserUsername}/details");
- res.Should().HaveStatusCodeOk()
- .And.Should().HaveBodyAsJson<UserDetail>()
- .Which.Should().BeEquivalentTo(d);
- }
-
- await GetAndTest(new UserDetail());
-
- {
- var res = await client.PatchAsJsonAsync($"users/{MockUsers.AdminUsername}/details", new UserDetail());
- res.Should().HaveStatusCode(HttpStatusCode.Forbidden)
- .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_Forbid);
- }
-
- {
- var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail
- {
- Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- QQ = "aaaaaaa",
- Email = "aaaaaa",
- PhoneNumber = "aaaaaaaa"
- });
- var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest)
- .And.Should().HaveBodyAsCommonResponse().Which;
- body.Code.Should().Be(CommonResponse.ErrorCodes.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/{MockUsers.UserUsername}/details", detail);
- res.Should().HaveStatusCodeOk();
- await GetAndTest(detail);
- }
-
- {
- var res = await client.GetAsync($"users/{MockUsers.UserUsername}/nickname");
- res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson<UserDetail>()
- .Which.Should().BeEquivalentTo(new UserDetail
- {
- Nickname = detail.Nickname
- });
- }
-
- var detail2 = new UserDetail
- {
- QQ = "",
- PhoneNumber = "12345678910",
- Description = "bbbbbbbb"
- };
-
- {
- var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail2);
- res.Should().HaveStatusCodeOk();
- 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/{MockUsers.UserUsername}/details", new UserDetail());
- res.Should().HaveStatusCodeOk();
- }
-
- {
- var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail());
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_UserNotExist);
- }
- }
- }
- }
-}
\ No newline at end of file diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..ec70b7e8 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,277 @@ +using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Models.Http;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Timeline.Tests.Mock.Data;
+using Xunit;
+using static Timeline.ErrorCodes.Http.User;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class UserTest : IClassFixture<WebApplicationFactory<Startup>>, IDisposable
+ {
+ private readonly TestApplication _testApp;
+ private readonly WebApplicationFactory<Startup> _factory;
+
+ public UserTest(WebApplicationFactory<Startup> factory)
+ {
+ _testApp = new TestApplication(factory);
+ _factory = _testApp.Factory;
+ }
+
+ public void Dispose()
+ {
+ _testApp.Dispose();
+ }
+
+ [Fact]
+ public async Task Get_List_Success()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.GetAsync("users");
+ res.Should().HaveStatusCode(200)
+ .And.Should().HaveJsonBody<UserInfo[]>()
+ .Which.Should().BeEquivalentTo(MockUser.UserInfoList);
+ }
+
+ [Fact]
+ public async Task Get_Single_Success()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.GetAsync("users/" + MockUser.User.Username);
+ res.Should().HaveStatusCode(200)
+ .And.Should().HaveJsonBody<UserInfo>()
+ .Which.Should().BeEquivalentTo(MockUser.User.Info);
+ }
+
+ [Fact]
+ public async Task Get_InvalidModel()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.GetAsync("users/aaa!a");
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Get_Users_404()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.GetAsync("users/usernotexist");
+ res.Should().HaveStatusCode(404)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Get.NotExist);
+ }
+
+ public static IEnumerable<object[]> Put_InvalidModel_Data()
+ {
+ yield return new object[] { "aaa", null, false };
+ yield return new object[] { "aaa", "p", null };
+ yield return new object[] { "aa!a", "p", false };
+ }
+
+ [Theory]
+ [MemberData(nameof(Put_InvalidModel_Data))]
+ public async Task Put_InvalidModel(string username, string password, bool? administrator)
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ (await client.PutAsJsonAsync("users/" + username,
+ new UserPutRequest { Password = password, Administrator = administrator }))
+ .Should().BeInvalidModel();
+ }
+
+ private async Task CheckAdministrator(HttpClient client, string username, bool administrator)
+ {
+ var res = await client.GetAsync("users/" + username);
+ res.Should().HaveStatusCode(200)
+ .And.Should().HaveJsonBody<UserInfo>()
+ .Which.Administrator.Should().Be(administrator);
+ }
+
+ [Fact]
+ public async Task Put_Modiefied()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.PutAsJsonAsync("users/" + MockUser.User.Username, new UserPutRequest
+ {
+ Password = "password",
+ Administrator = false
+ });
+ res.Should().BePut(false);
+ await CheckAdministrator(client, MockUser.User.Username, false);
+ }
+
+ [Fact]
+ public async Task Put_Created()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ const string username = "puttest";
+ const string url = "users/" + username;
+
+ var res = await client.PutAsJsonAsync(url, new UserPutRequest
+ {
+ Password = "password",
+ Administrator = false
+ });
+ res.Should().BePut(true);
+ await CheckAdministrator(client, username, false);
+ }
+
+ [Fact]
+ public async Task Patch_NotExist()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { });
+ res.Should().HaveStatusCode(404)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Patch.NotExist);
+ }
+
+ [Fact]
+ public async Task Patch_InvalidModel()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { });
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Patch_Success()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ {
+ var res = await client.PatchAsJsonAsync("users/" + MockUser.User.Username,
+ new UserPatchRequest { Administrator = false });
+ res.Should().HaveStatusCode(200);
+ await CheckAdministrator(client, MockUser.User.Username, false);
+ }
+ }
+
+ [Fact]
+ public async Task Delete_InvalidModel()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var url = "users/aaa!a";
+ var res = await client.DeleteAsync(url);
+ res.Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Delete_Deleted()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var url = "users/" + MockUser.User.Username;
+ var res = await client.DeleteAsync(url);
+ res.Should().BeDelete(true);
+
+ var res2 = await client.GetAsync(url);
+ res2.Should().HaveStatusCode(404);
+ }
+
+ [Fact]
+ public async Task Delete_NotExist()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.DeleteAsync("users/usernotexist");
+ res.Should().BeDelete(false);
+ }
+
+ private const string changeUsernameUrl = "userop/changeusername";
+
+ public static IEnumerable<object[]> Op_ChangeUsername_InvalidModel_Data()
+ {
+ yield return new[] { null, "uuu" };
+ yield return new[] { "uuu", null };
+ yield return new[] { "a!a", "uuu" };
+ yield return new[] { "uuu", "a!a" };
+ }
+
+ [Theory]
+ [MemberData(nameof(Op_ChangeUsername_InvalidModel_Data))]
+ public async Task Op_ChangeUsername_InvalidModel(string oldUsername, string newUsername)
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ (await client.PostAsJsonAsync(changeUsernameUrl,
+ new ChangeUsernameRequest { OldUsername = oldUsername, NewUsername = newUsername }))
+ .Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Op_ChangeUsername_UserNotExist()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.PostAsJsonAsync(changeUsernameUrl,
+ new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" });
+ res.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Op.ChangeUsername.NotExist);
+ }
+
+ [Fact]
+ public async Task Op_ChangeUsername_UserAlreadyExist()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ var res = await client.PostAsJsonAsync(changeUsernameUrl,
+ new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username });
+ res.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist);
+ }
+
+ [Fact]
+ public async Task Op_ChangeUsername_Success()
+ {
+ using var client = await _factory.CreateClientAsAdmin();
+ const string newUsername = "hahaha";
+ var res = await client.PostAsJsonAsync(changeUsernameUrl,
+ new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = newUsername });
+ res.Should().HaveStatusCode(200);
+ await client.CreateUserTokenAsync(newUsername, MockUser.User.Password);
+ }
+
+ private const string changePasswordUrl = "userop/changepassword";
+
+ public static IEnumerable<object[]> Op_ChangePassword_InvalidModel_Data()
+ {
+ yield return new[] { null, "ppp" };
+ yield return new[] { "ppp", null };
+ }
+
+ [Theory]
+ [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))]
+ public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword)
+ {
+ using var client = await _factory.CreateClientAsUser();
+ (await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }))
+ .Should().BeInvalidModel();
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_BadOldPassword()
+ {
+ using var client = await _factory.CreateClientAsUser();
+ var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
+ res.Should().HaveStatusCode(400)
+ .And.Should().HaveCommonBody()
+ .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword);
+ }
+
+ [Fact]
+ public async Task Op_ChangePassword_Success()
+ {
+ using var client = await _factory.CreateClientAsUser();
+ const string newPassword = "new";
+ var res = await client.PostAsJsonAsync(changePasswordUrl,
+ new ChangePasswordRequest { OldPassword = MockUser.User.Password, NewPassword = newPassword });
+ res.Should().HaveStatusCode(200);
+ await _factory.CreateDefaultClient() // don't use client above, because it sets authorization header
+ .CreateUserTokenAsync(MockUser.User.Username, newPassword);
+ }
+ }
+}
diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs index dd04f8f9..1e662546 100644 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ b/Timeline.Tests/Mock/Data/TestDatabase.cs @@ -10,36 +10,33 @@ namespace Timeline.Tests.Mock.Data public static void InitDatabase(DatabaseContext context)
{
context.Database.EnsureCreated();
- context.Users.AddRange(MockUsers.CreateMockUsers());
+ context.Users.AddRange(MockUser.CreateMockEntities());
context.SaveChanges();
}
- private readonly SqliteConnection _databaseConnection;
- private readonly DatabaseContext _databaseContext;
-
public TestDatabase()
{
- _databaseConnection = new SqliteConnection("Data Source=:memory:;");
- _databaseConnection.Open();
+ DatabaseConnection = new SqliteConnection("Data Source=:memory:;");
+ DatabaseConnection.Open();
var options = new DbContextOptionsBuilder<DatabaseContext>()
- .UseSqlite(_databaseConnection)
+ .UseSqlite(DatabaseConnection)
.Options;
- _databaseContext = new DatabaseContext(options);
+ DatabaseContext = new DatabaseContext(options);
- InitDatabase(_databaseContext);
+ InitDatabase(DatabaseContext);
}
public void Dispose()
{
- _databaseContext.Dispose();
+ DatabaseContext.Dispose();
- _databaseConnection.Close();
- _databaseConnection.Dispose();
+ DatabaseConnection.Close();
+ DatabaseConnection.Dispose();
}
- public SqliteConnection DatabaseConnection => _databaseConnection;
- public DatabaseContext DatabaseContext => _databaseContext;
+ public SqliteConnection DatabaseConnection { get; }
+ public DatabaseContext DatabaseContext { get; }
}
}
diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index 378fc280..fa75236a 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -1,52 +1,50 @@ using System;
using System.Collections.Generic;
-using System.Linq;
using Timeline.Entities;
using Timeline.Models;
using Timeline.Services;
namespace Timeline.Tests.Mock.Data
{
- public static class MockUsers
+ public class MockUser
{
- static MockUsers()
+ public MockUser(string username, string password, bool administrator)
{
- var mockUserInfos = CreateMockUsers().Select(u => UserUtility.CreateUserInfo(u)).ToList();
- UserUserInfo = mockUserInfos[0];
- AdminUserInfo = mockUserInfos[1];
- UserInfos = mockUserInfos;
+ Info = new UserInfo(username, administrator);
+ Password = password;
}
- public const string UserUsername = "user";
- public const string AdminUsername = "admin";
- public const string UserPassword = "user";
- public const string AdminPassword = "admin";
+ public UserInfo Info { get; set; }
+ public string Username => Info.Username;
+ public string Password { get; set; }
+ public bool Administrator => Info.Administrator;
- // emmmmmmm. Never reuse the user instances because EF Core uses them which will cause strange things.
- internal static IEnumerable<User> CreateMockUsers()
+
+ public static MockUser User { get; } = new MockUser("user", "userpassword", false);
+ public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true);
+
+ public static IReadOnlyList<UserInfo> UserInfoList { get; } = new List<UserInfo> { User.Info, Admin.Info };
+
+ // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things.
+ public static IEnumerable<User> CreateMockEntities()
{
- var users = new List<User>();
var passwordService = new PasswordService();
- users.Add(new User
+ User Create(MockUser user)
{
- Name = UserUsername,
- EncryptedPassword = passwordService.HashPassword(UserPassword),
- RoleString = UserUtility.IsAdminToRoleString(false),
- Avatar = UserAvatar.Create(DateTime.Now)
- });
- users.Add(new User
+ return new User
+ {
+ Name = user.Username,
+ EncryptedPassword = passwordService.HashPassword(user.Password),
+ RoleString = UserRoleConvert.ToString(user.Administrator),
+ Avatar = null
+ };
+ }
+
+ return new List<User>
{
- Name = AdminUsername,
- EncryptedPassword = passwordService.HashPassword(AdminPassword),
- RoleString = UserUtility.IsAdminToRoleString(true),
- Avatar = UserAvatar.Create(DateTime.Now)
- });
- return users;
+ Create(User),
+ Create(Admin)
+ };
}
-
- public static IReadOnlyList<UserInfo> UserInfos { get; }
-
- public static UserInfo AdminUserInfo { get; }
- public static UserInfo UserUserInfo { get; }
}
}
diff --git a/Timeline.Tests/Mock/Services/TestClock.cs b/Timeline.Tests/Mock/Services/TestClock.cs index 0082171e..6671395a 100644 --- a/Timeline.Tests/Mock/Services/TestClock.cs +++ b/Timeline.Tests/Mock/Services/TestClock.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
using System;
using Timeline.Services;
@@ -14,12 +12,4 @@ namespace Timeline.Tests.Mock.Services return MockCurrentTime.GetValueOrDefault(DateTime.Now);
}
}
-
- public static class TestClockWebApplicationFactoryExtensions
- {
- public static TestClock GetTestClock<T>(this WebApplicationFactory<T> factory) where T : class
- {
- return factory.Server.Host.Services.GetRequiredService<IClock>() as TestClock;
- }
- }
}
diff --git a/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs b/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs new file mode 100644 index 00000000..4084dd8f --- /dev/null +++ b/Timeline.Tests/Mock/Services/TestStringLocalizerFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Timeline.Tests.Mock.Services
+{
+ internal static class TestStringLocalizerFactory
+ {
+ internal static IStringLocalizerFactory Create()
+ {
+ return new ResourceManagerStringLocalizerFactory(
+ Options.Create(new LocalizationOptions()
+ {
+ ResourcesPath = "Resource"
+ }),
+ NullLoggerFactory.Instance
+ );
+ }
+
+ internal static IStringLocalizer<T> Create<T>(this IStringLocalizerFactory factory)
+ {
+ return new StringLocalizer<T>(factory);
+ }
+ }
+}
diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 1852da5f..497a00b7 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -1,27 +1,34 @@ <Project Sdk="Microsoft.NET.Sdk.Web">
- <PropertyGroup>
- <TargetFramework>netcoreapp3.0</TargetFramework>
- </PropertyGroup>
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
- <ItemGroup>
- <PackageReference Include="coverlet.collector" Version="1.1.0">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- </PackageReference>
- <PackageReference Include="FluentAssertions" Version="5.9.0" />
- <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
- <PackageReference Include="xunit" Version="2.4.1" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- </ItemGroup>
+ <LangVersion>8.0</LangVersion>
+ </PropertyGroup>
- <ItemGroup>
- <ProjectReference Include="..\Timeline\Timeline.csproj" />
- </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="coverlet.collector" Version="1.1.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="FluentAssertions" Version="5.9.0" />
+ <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.6">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
+ <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline\Timeline.csproj" />
+ </ItemGroup>
</Project>
diff --git a/Timeline.Tests/TokenUnitTest.cs b/Timeline.Tests/TokenUnitTest.cs deleted file mode 100644 index 3babacf7..00000000 --- a/Timeline.Tests/TokenUnitTest.cs +++ /dev/null @@ -1,190 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using System.Net.Http;
-using Timeline.Controllers;
-using Timeline.Models.Http;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Timeline.Tests.Helpers.Authentication;
-using Timeline.Tests.Mock.Data;
-using Timeline.Tests.Mock.Services;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests
-{
- public class TokenUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private const string CreateTokenUrl = "token/create";
- private const string VerifyTokenUrl = "token/verify";
-
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public TokenUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async void CreateToken_InvalidModel()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- // missing username
- await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl,
- new CreateTokenRequest { Username = null, Password = "user" });
- // missing password
- await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl,
- new CreateTokenRequest { Username = "user", Password = null });
- // bad expire offset
- await InvalidModelTestHelpers.TestPostInvalidModel(client, CreateTokenUrl,
- new CreateTokenRequest
- {
- Username = MockUsers.UserUsername,
- Password = MockUsers.UserPassword,
- ExpireOffset = -1000
- });
- }
- }
-
- [Fact]
- public async void CreateToken_UserNotExist()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl,
- new CreateTokenRequest { Username = "usernotexist", Password = "???" });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Create_UserNotExist);
- }
- }
-
- [Fact]
- public async void CreateToken_BadPassword()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl,
- new CreateTokenRequest { Username = MockUsers.UserUsername, Password = "???" });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Create_BadPassword);
- }
- }
-
- [Fact]
- public async void CreateToken_Success()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl,
- new CreateTokenRequest { Username = MockUsers.UserUsername, Password = MockUsers.UserPassword });
- var body = response.Should().HaveStatusCodeOk()
- .And.Should().HaveBodyAsJson<CreateTokenResponse>().Which;
- body.Token.Should().NotBeNullOrWhiteSpace();
- body.User.Should().BeEquivalentTo(MockUsers.UserUserInfo);
- }
- }
-
- [Fact]
- public async void VerifyToken_InvalidModel()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- // missing token
- await InvalidModelTestHelpers.TestPostInvalidModel(client, VerifyTokenUrl,
- new VerifyTokenRequest { Token = null });
- }
- }
-
- [Fact]
- public async void VerifyToken_BadToken()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_BadToken);
- }
- }
-
- [Fact]
- public async void VerifyToken_BadVersion()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).Token;
-
- using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
- {
- // create a user for test
- var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
- await userService.PatchUser(MockUsers.UserUsername, null, null);
- }
-
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_BadVersion);
- }
- }
-
- [Fact]
- public async void VerifyToken_UserNotExist()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword)).Token;
-
- using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
- {
- // create a user for test
- var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
- await userService.DeleteUser(MockUsers.UserUsername);
- }
-
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_UserNotExist);
- }
- }
-
- [Fact]
- public async void VerifyToken_Expired()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- // I can only control the token expired time but not current time
- // because verify logic is encapsuled in other library.
- var mockClock = _factory.GetTestClock();
- mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2);
- var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token;
- var response = await client.PostAsJsonAsync(VerifyTokenUrl,
- new VerifyTokenRequest { Token = token });
- response.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired);
- mockClock.MockCurrentTime = null;
- }
- }
-
- [Fact]
- public async void VerifyToken_Success()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var createTokenResult = await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword);
- var response = await client.PostAsJsonAsync(VerifyTokenUrl,
- new VerifyTokenRequest { Token = createTokenResult.Token });
- response.Should().HaveStatusCodeOk()
- .And.Should().HaveBodyAsJson<VerifyTokenResponse>()
- .Which.User.Should().BeEquivalentTo(MockUsers.UserUserInfo);
- }
- }
- }
-}
diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index 93bb70ae..1f71f6f6 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -1,8 +1,11 @@ using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using SixLabors.ImageSharp.Formats.Png;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -10,40 +13,12 @@ using Timeline.Entities; using Timeline.Services;
using Timeline.Tests.Helpers;
using Timeline.Tests.Mock.Data;
+using Timeline.Tests.Mock.Services;
using Xunit;
using Xunit.Abstractions;
namespace Timeline.Tests
{
- public class MockDefaultUserAvatarProvider : IDefaultUserAvatarProvider
- {
- public static string ETag { get; } = "Hahaha";
-
- public static AvatarInfo AvatarInfo { get; } = new AvatarInfo
- {
- Avatar = new Avatar { Type = "image/test", Data = Encoding.ASCII.GetBytes("test") },
- LastModified = DateTime.Now
- };
-
- public Task<string> GetDefaultAvatarETag()
- {
- return Task.FromResult(ETag);
- }
-
- public Task<AvatarInfo> GetDefaultAvatar()
- {
- return Task.FromResult(AvatarInfo);
- }
- }
-
- public class MockUserAvatarValidator : IUserAvatarValidator
- {
- public Task Validate(Avatar avatar)
- {
- return Task.CompletedTask;
- }
- }
-
public class UserAvatarValidatorTest : IClassFixture<UserAvatarValidator>
{
private readonly UserAvatarValidator _validator;
@@ -62,8 +37,8 @@ namespace Timeline.Tests Type = "image/png"
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.CantDecode);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode);
}
[Fact]
@@ -75,8 +50,8 @@ namespace Timeline.Tests Type = "image/jpeg"
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.UnmatchedFormat);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat);
}
[Fact]
@@ -88,8 +63,8 @@ namespace Timeline.Tests Type = PngFormat.Instance.DefaultMimeType
};
_validator.Awaiting(v => v.Validate(avatar))
- .Should().Throw<AvatarDataException>()
- .Where(e => e.Avatar == avatar && e.Error == AvatarDataException.ErrorReason.BadSize);
+ .Should().Throw<AvatarFormatException>()
+ .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize);
}
[Fact]
@@ -105,25 +80,33 @@ namespace Timeline.Tests }
}
- public class UserAvatarServiceTest : IDisposable, IClassFixture<MockDefaultUserAvatarProvider>, IClassFixture<MockUserAvatarValidator>
+ public class UserAvatarServiceTest : IDisposable
{
- private UserAvatar MockAvatarEntity1 { get; } = new UserAvatar
+ private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar
+ {
+ Type = $"image/test{key}",
+ Data = Encoding.ASCII.GetBytes($"mock{key}"),
+ ETag = $"etag{key}",
+ LastModified = DateTime.Now
+ };
+
+ private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo
{
- Type = "image/testaaa",
- Data = Encoding.ASCII.GetBytes("amock"),
- ETag = "aaaa",
+ Avatar = new Avatar
+ {
+ Type = $"image/test{key}",
+ Data = Encoding.ASCII.GetBytes($"mock{key}")
+ },
LastModified = DateTime.Now
};
- private UserAvatar MockAvatarEntity2 { get; } = new UserAvatar
+ private Avatar CreateMockAvatar(string key) => new Avatar
{
- Type = "image/testbbb",
- Data = Encoding.ASCII.GetBytes("bmock"),
- ETag = "bbbb",
- LastModified = DateTime.Now + TimeSpan.FromMinutes(1)
+ Type = $"image/test{key}",
+ Data = Encoding.ASCII.GetBytes($"mock{key}")
};
- private Avatar ToAvatar(UserAvatar entity)
+ private static Avatar ToAvatar(UserAvatar entity)
{
return new Avatar
{
@@ -132,7 +115,7 @@ namespace Timeline.Tests };
}
- private AvatarInfo ToAvatarInfo(UserAvatar entity)
+ private static AvatarInfo ToAvatarInfo(UserAvatar entity)
{
return new AvatarInfo
{
@@ -141,177 +124,161 @@ namespace Timeline.Tests };
}
- private void Set(UserAvatar to, UserAvatar from)
- {
- to.Type = from.Type;
- to.Data = from.Data;
- to.ETag = from.ETag;
- to.LastModified = from.LastModified;
- }
-
- private readonly MockDefaultUserAvatarProvider _mockDefaultUserAvatarProvider;
+ private readonly Mock<IDefaultUserAvatarProvider> _mockDefaultAvatarProvider;
+ private readonly Mock<IUserAvatarValidator> _mockValidator;
+ private readonly Mock<IETagGenerator> _mockETagGenerator;
+ private readonly Mock<IClock> _mockClock;
- private readonly ILoggerFactory _loggerFactory;
private readonly TestDatabase _database;
- private readonly IETagGenerator _eTagGenerator;
-
private readonly UserAvatarService _service;
- public UserAvatarServiceTest(ITestOutputHelper outputHelper, MockDefaultUserAvatarProvider mockDefaultUserAvatarProvider, MockUserAvatarValidator mockUserAvatarValidator)
+ public UserAvatarServiceTest()
{
- _mockDefaultUserAvatarProvider = mockDefaultUserAvatarProvider;
+ _mockDefaultAvatarProvider = new Mock<IDefaultUserAvatarProvider>();
+ _mockValidator = new Mock<IUserAvatarValidator>();
+ _mockETagGenerator = new Mock<IETagGenerator>();
+ _mockClock = new Mock<IClock>();
- _loggerFactory = Logging.Create(outputHelper);
_database = new TestDatabase();
- _eTagGenerator = new ETagGenerator();
-
- _service = new UserAvatarService(_loggerFactory.CreateLogger<UserAvatarService>(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator);
+ _service = new UserAvatarService(NullLogger<UserAvatarService>.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object);
}
public void Dispose()
{
- _loggerFactory.Dispose();
_database.Dispose();
}
- [Fact]
- public void GetAvatarETag_ShouldThrow_ArgumentException()
- {
- // no need to await because arguments are checked syncronizedly.
- _service.Invoking(s => s.GetAvatarETag(null)).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.GetAvatarETag("")).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
- }
-
- [Fact]
- public void GetAvatarETag_ShouldThrow_UserNotExistException()
+ [Theory]
+ [InlineData(null, typeof(ArgumentNullException))]
+ [InlineData("", typeof(UsernameBadFormatException))]
+ [InlineData("a!a", typeof(UsernameBadFormatException))]
+ [InlineData("usernotexist", typeof(UserNotExistException))]
+ public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType)
{
- const string username = "usernotexist";
- _service.Awaiting(s => s.GetAvatarETag(username)).Should().Throw<UserNotExistException>()
- .Where(e => e.Username == username);
+ await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType);
}
[Fact]
public async Task GetAvatarETag_ShouldReturn_Default()
{
- const string username = MockUsers.UserUsername;
- (await _service.GetAvatarETag(username)).Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatarETag()));
+ const string etag = "aaaaaa";
+ _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag);
+ (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag);
}
[Fact]
public async Task GetAvatarETag_ShouldReturn_Data()
{
- const string username = MockUsers.UserUsername;
+ string username = MockUser.User.Username;
+ var mockAvatarEntity = CreateMockAvatarEntity("aaa");
{
- // create mock data
var context = _database.DatabaseContext;
var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
- Set(user.Avatar, MockAvatarEntity1);
+ user.Avatar = mockAvatarEntity;
await context.SaveChangesAsync();
}
-
- (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(MockAvatarEntity1.ETag);
+ (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag);
}
- [Fact]
- public void GetAvatar_ShouldThrow_ArgumentException()
+ [Theory]
+ [InlineData(null, typeof(ArgumentNullException))]
+ [InlineData("", typeof(UsernameBadFormatException))]
+ [InlineData("a!a", typeof(UsernameBadFormatException))]
+ [InlineData("usernotexist", typeof(UserNotExistException))]
+ public async Task GetAvatar_ShouldThrow(string username, Type exceptionType)
{
- // no need to await because arguments are checked syncronizedly.
- _service.Invoking(s => s.GetAvatar(null)).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.GetAvatar("")).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
- }
+ await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType);
- [Fact]
- public void GetAvatar_ShouldThrow_UserNotExistException()
- {
- const string username = "usernotexist";
- _service.Awaiting(s => s.GetAvatar(username)).Should().Throw<UserNotExistException>()
- .Where(e => e.Username == username);
}
[Fact]
public async Task GetAvatar_ShouldReturn_Default()
{
- const string username = MockUsers.UserUsername;
- (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatar()).Avatar);
+ var mockAvatar = CreateMockAvatarInfo("aaa");
+ _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar);
+ string username = MockUser.User.Username;
+ (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar);
}
[Fact]
public async Task GetAvatar_ShouldReturn_Data()
{
- const string username = MockUsers.UserUsername;
-
+ string username = MockUser.User.Username;
+ var mockAvatarEntity = CreateMockAvatarEntity("aaa");
{
- // create mock data
var context = _database.DatabaseContext;
var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
- Set(user.Avatar, MockAvatarEntity1);
+ user.Avatar = mockAvatarEntity;
await context.SaveChangesAsync();
}
- (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(MockAvatarEntity1));
+ (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity));
}
- [Fact]
- public void SetAvatar_ShouldThrow_ArgumentException()
+ public static IEnumerable<object[]> SetAvatar_ShouldThrow_Data()
{
- var avatar = ToAvatar(MockAvatarEntity1);
- // no need to await because arguments are checked syncronizedly.
- _service.Invoking(s => s.SetAvatar(null, avatar)).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.SetAvatar("", avatar)).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
-
- _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = null, Data = new[] { (byte)0x00 } })).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "avatar" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = "", Data = new[] { (byte)0x00 } })).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "avatar" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
-
- _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = "aaa", Data = null })).Should().Throw<ArgumentException>()
- .Where(e => e.ParamName == "avatar" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ yield return new object[] { null, null, typeof(ArgumentNullException) };
+ yield return new object[] { "", null, typeof(UsernameBadFormatException) };
+ yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) };
+ yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) };
+ yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) };
+ yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) };
+ yield return new object[] { "usernotexist", null, typeof(UserNotExistException) };
}
- [Fact]
- public void SetAvatar_ShouldThrow_UserNotExistException()
+ [Theory]
+ [MemberData(nameof(SetAvatar_ShouldThrow_Data))]
+ public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType)
{
- const string username = "usernotexist";
- _service.Awaiting(s => s.SetAvatar(username, ToAvatar(MockAvatarEntity1))).Should().Throw<UserNotExistException>()
- .Where(e => e.Username == username);
+ await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType);
}
[Fact]
public async Task SetAvatar_Should_Work()
{
- const string username = MockUsers.UserUsername;
+ string username = MockUser.User.Username;
var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync();
+ var avatar1 = CreateMockAvatar("aaa");
+ var avatar2 = CreateMockAvatar("bbb");
+
+ string etag1 = "etagaaa";
+ string etag2 = "etagbbb";
+
+ DateTime dateTime1 = DateTime.Now.AddSeconds(2);
+ DateTime dateTime2 = DateTime.Now.AddSeconds(10);
+ DateTime dateTime3 = DateTime.Now.AddSeconds(20);
+
// create
- var avatar1 = ToAvatar(MockAvatarEntity1);
+ _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1);
+ _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1);
await _service.SetAvatar(username, avatar1);
user.Avatar.Should().NotBeNull();
user.Avatar.Type.Should().Be(avatar1.Type);
user.Avatar.Data.Should().Equal(avatar1.Data);
- user.Avatar.ETag.Should().NotBeNull();
+ user.Avatar.ETag.Should().Be(etag1);
+ user.Avatar.LastModified.Should().Be(dateTime1);
// modify
- var avatar2 = ToAvatar(MockAvatarEntity2);
+ _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2);
+ _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2);
await _service.SetAvatar(username, avatar2);
user.Avatar.Should().NotBeNull();
- user.Avatar.Type.Should().Be(MockAvatarEntity2.Type);
- user.Avatar.Data.Should().Equal(MockAvatarEntity2.Data);
- user.Avatar.ETag.Should().NotBeNull();
+ user.Avatar.Type.Should().Be(avatar2.Type);
+ user.Avatar.Data.Should().Equal(avatar2.Data);
+ user.Avatar.ETag.Should().Be(etag2);
+ user.Avatar.LastModified.Should().Be(dateTime2);
// delete
+ _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3);
await _service.SetAvatar(username, null);
user.Avatar.Type.Should().BeNull();
user.Avatar.Data.Should().BeNull();
user.Avatar.ETag.Should().BeNull();
+ user.Avatar.LastModified.Should().Be(dateTime3);
}
}
}
diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs deleted file mode 100644 index 98613429..00000000 --- a/Timeline.Tests/UserDetailServiceTest.cs +++ /dev/null @@ -1,275 +0,0 @@ -using FluentAssertions;
-using Microsoft.Extensions.Logging;
-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;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests
-{
- public class UserDetailServiceTest : IDisposable
- {
- private readonly ILoggerFactory _loggerFactory;
- private readonly TestDatabase _database;
-
- private readonly UserDetailService _service;
-
- public UserDetailServiceTest(ITestOutputHelper outputHelper)
- {
- _loggerFactory = Logging.Create(outputHelper);
- _database = new TestDatabase();
-
- _service = new UserDetailService(_loggerFactory.CreateLogger<UserDetailService>(), _database.DatabaseContext);
- }
-
- public void Dispose()
- {
- _loggerFactory.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(MockUsers.UserUsername);
- nickname.Should().BeNull();
- }
-
- {
- var context = _database.DatabaseContext;
- var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
- 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, MockUsers.UserUsername);
- var entity = new UserDetailEntity
- {
- Nickname = nickname,
- UserId = userId
- };
- context.Add(entity);
- await context.SaveChangesAsync();
- }
-
- {
- var n = await _service.GetUserNickname(MockUsers.UserUsername);
- 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(MockUsers.UserUsername);
- detail.Should().BeEquivalentTo(new UserDetail());
- }
-
- {
- var context = _database.DatabaseContext;
- var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
- 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, MockUsers.UserUsername);
- var entity = new UserDetailEntity
- {
- Email = email,
- Description = description,
- UserId = userId
- };
- context.Add(entity);
- await context.SaveChangesAsync();
- }
-
- {
- var detail = await _service.GetUserDetail(MockUsers.UserUsername);
- 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(MockUsers.UserUsername, new UserDetail());
-
- var context = _database.DatabaseContext;
- var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
- 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(MockUsers.UserUsername, CreateWith(mockData1));
-
- var context = _database.DatabaseContext;
- var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
- 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(MockUsers.UserUsername, CreateWith(mockData2));
- TestWith(mockData2);
- await _service.UpdateUserDetail(MockUsers.UserUsername, 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(MockUsers.UserUsername, detail);
-
- var context = _database.DatabaseContext;
- var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername);
- 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(MockUsers.UserUsername, 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/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs deleted file mode 100644 index 77ec37ee..00000000 --- a/Timeline.Tests/UserUnitTest.cs +++ /dev/null @@ -1,318 +0,0 @@ -using FluentAssertions;
-using Microsoft.AspNetCore.Mvc.Testing;
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Timeline.Controllers;
-using Timeline.Models;
-using Timeline.Models.Http;
-using Timeline.Tests.Helpers;
-using Timeline.Tests.Helpers.Authentication;
-using Timeline.Tests.Mock.Data;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests
-{
- public class UserUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public UserUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async Task Get_Users_List()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.GetAsync("users");
- res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson<UserInfo[]>()
- .Which.Should().BeEquivalentTo(MockUsers.UserInfos);
- }
- }
-
- [Fact]
- public async Task Get_Users_User()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.GetAsync("users/" + MockUsers.UserUsername);
- res.Should().HaveStatusCodeOk()
- .And.Should().HaveBodyAsJson<UserInfo>()
- .Which.Should().BeEquivalentTo(MockUsers.UserUserInfo);
- }
- }
-
- [Fact]
- public async Task Get_Users_404()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.GetAsync("users/usernotexist");
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Get_NotExist);
- }
- }
-
- [Fact]
- public async Task Put_InvalidModel()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- const string url = "users/aaaaaaaa";
- // missing password
- await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = null, Administrator = false });
- // missing administrator
- await InvalidModelTestHelpers.TestPutInvalidModel(client, url, new UserPutRequest { Password = "???", Administrator = null });
- }
- }
-
- [Fact]
- public async Task Put_BadUsername()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.PutAsJsonAsync("users/dsf fddf", new UserPutRequest
- {
- Password = "???",
- Administrator = false
- });
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Put_BadUsername);
- }
- }
-
- private async Task CheckAdministrator(HttpClient client, string username, bool administrator)
- {
- var res = await client.GetAsync("users/" + username);
- res.Should().HaveStatusCodeOk()
- .And.Should().HaveBodyAsJson<UserInfo>()
- .Which.Administrator.Should().Be(administrator);
- }
-
- [Fact]
- public async Task Put_Modiefied()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.PutAsJsonAsync("users/" + MockUsers.UserUsername, new UserPutRequest
- {
- Password = "password",
- Administrator = false
- });
- res.Should().BePutModified();
- await CheckAdministrator(client, MockUsers.UserUsername, false);
- }
- }
-
- [Fact]
- public async Task Put_Created()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- const string username = "puttest";
- const string url = "users/" + username;
-
- var res = await client.PutAsJsonAsync(url, new UserPutRequest
- {
- Password = "password",
- Administrator = false
- });
- res.Should().BePutCreated();
- await CheckAdministrator(client, username, false);
- }
- }
-
- [Fact]
- public async Task Patch_NotExist()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { });
- res.Should().HaveStatusCodeNotFound()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.Patch_NotExist);
- }
- }
-
- [Fact]
- public async Task Patch_Success()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- {
- var res = await client.PatchAsJsonAsync("users/" + MockUsers.UserUsername,
- new UserPatchRequest { Administrator = false });
- res.Should().HaveStatusCodeOk();
- await CheckAdministrator(client, MockUsers.UserUsername, false);
- }
- }
- }
-
- [Fact]
- public async Task Delete_Deleted()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- {
- var url = "users/" + MockUsers.UserUsername;
- var res = await client.DeleteAsync(url);
- res.Should().BeDeleteDeleted();
-
- var res2 = await client.GetAsync(url);
- res2.Should().HaveStatusCodeNotFound();
- }
- }
- }
-
- [Fact]
- public async Task Delete_NotExist()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- {
- var res = await client.DeleteAsync("users/usernotexist");
- res.Should().BeDeleteNotExist();
- }
- }
- }
-
-
- public class ChangeUsernameUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private const string url = "userop/changeusername";
-
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public ChangeUsernameUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async Task InvalidModel()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- // missing old username
- await InvalidModelTestHelpers.TestPostInvalidModel(client, url,
- new ChangeUsernameRequest { OldUsername= null, NewUsername= "hhh" });
- // missing new username
- await InvalidModelTestHelpers.TestPostInvalidModel(client, url,
- new ChangeUsernameRequest { OldUsername= "hhh", NewUsername= null });
- // bad username
- await InvalidModelTestHelpers.TestPostInvalidModel(client, url,
- new ChangeUsernameRequest { OldUsername = "hhh", NewUsername = "???" });
- }
- }
-
- [Fact]
- public async Task UserNotExist()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.PostAsJsonAsync(url,
- new ChangeUsernameRequest{ OldUsername= "usernotexist", NewUsername= "newUsername" });
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangeUsername_NotExist);
- }
- }
-
- [Fact]
- public async Task UserAlreadyExist()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- var res = await client.PostAsJsonAsync(url,
- new ChangeUsernameRequest { OldUsername = MockUsers.UserUsername, NewUsername = MockUsers.AdminUsername });
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangeUsername_AlreadyExist);
- }
- }
-
- [Fact]
- public async Task Success()
- {
- using (var client = await _factory.CreateClientAsAdmin())
- {
- const string newUsername = "hahaha";
- var res = await client.PostAsJsonAsync(url,
- new ChangeUsernameRequest { OldUsername = MockUsers.UserUsername, NewUsername = newUsername });
- res.Should().HaveStatusCodeOk();
- await client.CreateUserTokenAsync(newUsername, MockUsers.UserPassword);
- }
- }
- }
-
-
- public class ChangePasswordUnitTest : IClassFixture<MyWebApplicationFactory<Startup>>, IDisposable
- {
- private const string url = "userop/changepassword";
-
- private readonly WebApplicationFactory<Startup> _factory;
- private readonly Action _disposeAction;
-
- public ChangePasswordUnitTest(MyWebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper, out _disposeAction);
- }
-
- public void Dispose()
- {
- _disposeAction();
- }
-
- [Fact]
- public async Task InvalidModel()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- // missing old password
- await InvalidModelTestHelpers.TestPostInvalidModel(client, url,
- new ChangePasswordRequest { OldPassword = null, NewPassword = "???" });
- // missing new password
- await InvalidModelTestHelpers.TestPostInvalidModel(client, url,
- new ChangePasswordRequest { OldPassword = "???", NewPassword = null });
- }
- }
-
- [Fact]
- public async Task BadOldPassword()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- var res = await client.PostAsJsonAsync(url, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
- res.Should().HaveStatusCodeBadRequest()
- .And.Should().HaveBodyAsCommonResponseWithCode(UserController.ErrorCodes.ChangePassword_BadOldPassword);
- }
- }
-
- [Fact]
- public async Task Success()
- {
- using (var client = await _factory.CreateClientAsUser())
- {
- const string newPassword = "new";
- var res = await client.PostAsJsonAsync(url,
- new ChangePasswordRequest { OldPassword = MockUsers.UserPassword, NewPassword = newPassword });
- res.Should().HaveStatusCodeOk();
- await client.CreateUserTokenAsync(MockUsers.UserUsername, newPassword);
- }
- }
- }
- }
-}
diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 6a635ba1..283e18e2 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -15,15 +15,9 @@ namespace Timeline.Tests private string FailAndMessage(string username)
{
- var result = _validator.Validate(username, out var message);
+ var (result, messageGenerator) = _validator.Validate(username);
result.Should().BeFalse();
- return message;
- }
-
- private void Succeed(string username)
- {
- _validator.Validate(username, out var message).Should().BeTrue();
- message.Should().Be(ValidationConstants.SuccessMessage);
+ return messageGenerator(null);
}
[Fact]
@@ -35,8 +29,9 @@ namespace Timeline.Tests [Fact]
public void NotString()
{
- var result = _validator.Validate(123, out var message);
+ var (result, messageGenerator) = _validator.Validate(123);
result.Should().BeFalse();
+ var message = messageGenerator(null);
message.Should().ContainEquivalentOf("type");
}
@@ -46,31 +41,14 @@ namespace Timeline.Tests FailAndMessage("").Should().ContainEquivalentOf("empty");
}
- [Fact]
- public void WhiteSpace()
+ [Theory]
+ [InlineData("!")]
+ [InlineData("!abc")]
+ [InlineData("ab c")]
+ public void BadCharactor(string value)
{
- FailAndMessage(" ").Should().ContainEquivalentOf("whitespace");
- FailAndMessage("\t").Should().ContainEquivalentOf("whitespace");
- FailAndMessage("\n").Should().ContainEquivalentOf("whitespace");
-
- FailAndMessage("a b").Should().ContainEquivalentOf("whitespace");
- FailAndMessage("a\tb").Should().ContainEquivalentOf("whitespace");
- FailAndMessage("a\nb").Should().ContainEquivalentOf("whitespace");
- }
-
- [Fact]
- public void BadCharactor()
- {
- FailAndMessage("!").Should().ContainEquivalentOf("regex");
- FailAndMessage("!abc").Should().ContainEquivalentOf("regex");
- FailAndMessage("ab!c").Should().ContainEquivalentOf("regex");
- }
-
- [Fact]
- public void BadBegin()
- {
- FailAndMessage("-").Should().ContainEquivalentOf("regex");
- FailAndMessage("-abc").Should().ContainEquivalentOf("regex");
+ FailAndMessage(value).Should().ContainEquivalentOf("invalid")
+ .And.ContainEquivalentOf("character");
}
[Fact]
@@ -79,14 +57,20 @@ namespace Timeline.Tests FailAndMessage(new string('a', 40)).Should().ContainEquivalentOf("long");
}
- [Fact]
- public void Success()
+ [Theory]
+ [InlineData("abc")]
+ [InlineData("-abc")]
+ [InlineData("_abc")]
+ [InlineData("abc-")]
+ [InlineData("abc_")]
+ [InlineData("a-bc")]
+ [InlineData("a-b-c")]
+ [InlineData("a-b_c")]
+ public void Success(string value)
{
- Succeed("abc");
- Succeed("_abc");
- Succeed("a-bc");
- Succeed("a-b-c");
- Succeed("a-b_c");
+
+ var (result, _) = _validator.Validate(value);
+ result.Should().BeTrue();
}
}
}
diff --git a/Timeline/Authenticate/PrincipalExtensions.cs b/Timeline/Authenticate/PrincipalExtensions.cs deleted file mode 100644 index fa39ea89..00000000 --- a/Timeline/Authenticate/PrincipalExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal;
-using Timeline.Entities;
-
-namespace Timeline.Authenticate
-{
- public static class PrincipalExtensions
- {
- public static bool IsAdmin(this IPrincipal principal)
- {
- return principal.IsInRole(UserRoles.Admin);
- }
- }
-}
diff --git a/Timeline/Authenticate/Attribute.cs b/Timeline/Authentication/Attribute.cs index 239a2a1c..370b37e1 100644 --- a/Timeline/Authenticate/Attribute.cs +++ b/Timeline/Authentication/Attribute.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization;
using Timeline.Entities;
-namespace Timeline.Authenticate
+namespace Timeline.Authentication
{
public class AdminAuthorizeAttribute : AuthorizeAttribute
{
diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authentication/AuthHandler.cs index f9409c1a..47ed1d71 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authentication/AuthHandler.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Timeline.Models;
using Timeline.Services;
-namespace Timeline.Authenticate
+namespace Timeline.Authentication
{
static class AuthConstants
{
@@ -18,7 +18,7 @@ namespace Timeline.Authenticate public const string DisplayName = "My Jwt Auth Scheme";
}
- class AuthOptions : AuthenticationSchemeOptions
+ public class AuthOptions : AuthenticationSchemeOptions
{
/// <summary>
/// The query param key to search for token. If null then query params are not searched for token. Default to <c>"token"</c>.
@@ -26,7 +26,7 @@ namespace Timeline.Authenticate public string TokenQueryParamKey { get; set; } = "token";
}
- class AuthHandler : AuthenticationHandler<AuthOptions>
+ public class AuthHandler : AuthenticationHandler<AuthOptions>
{
private readonly ILogger<AuthHandler> _logger;
private readonly IUserService _userService;
@@ -39,14 +39,14 @@ namespace Timeline.Authenticate }
// return null if no token is found
- private string ExtractToken()
+ private string? ExtractToken()
{
// check the authorization header
string header = Request.Headers[HeaderNames.Authorization];
- if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase))
{
var token = header.Substring("Bearer ".Length).Trim();
- _logger.LogInformation("Token is found in authorization header. Token is {} .", token);
+ _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenFoundInHeader, token);
return token;
}
@@ -57,7 +57,7 @@ namespace Timeline.Authenticate string token = Request.Query[paramQueryKey];
if (!string.IsNullOrEmpty(token))
{
- _logger.LogInformation("Token is found in query param with key \"{}\". Token is {} .", paramQueryKey, token);
+ _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenFoundInQuery, paramQueryKey, token);
return token;
}
}
@@ -71,7 +71,7 @@ namespace Timeline.Authenticate var token = ExtractToken();
if (string.IsNullOrEmpty(token))
{
- _logger.LogInformation("No jwt token is found.");
+ _logger.LogInformation(Resources.Authentication.AuthHandler.LogTokenNotFound);
return AuthenticateResult.NoResult();
}
@@ -81,20 +81,16 @@ namespace Timeline.Authenticate var identity = new ClaimsIdentity(AuthConstants.Scheme);
identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String));
- identity.AddClaims(UserUtility.IsAdminToRoleArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String)));
+ identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String)));
var principal = new ClaimsPrincipal();
principal.AddIdentity(identity);
return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme));
}
- catch (ArgumentException)
+ catch (Exception e) when (e! is ArgumentException)
{
- throw; // this exception usually means server error.
- }
- catch (Exception e)
- {
- _logger.LogInformation(e, "A jwt token validation failed.");
+ _logger.LogInformation(e, Resources.Authentication.AuthHandler.LogTokenValidationFail);
return AuthenticateResult.Fail(e);
}
}
diff --git a/Timeline/Authentication/PrincipalExtensions.cs b/Timeline/Authentication/PrincipalExtensions.cs new file mode 100644 index 00000000..8d77ab62 --- /dev/null +++ b/Timeline/Authentication/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal;
+using Timeline.Entities;
+
+namespace Timeline.Authentication
+{
+ internal static class PrincipalExtensions
+ {
+ internal static bool IsAdministrator(this IPrincipal principal)
+ {
+ return principal.IsInRole(UserRoles.Admin);
+ }
+ }
+}
diff --git a/Timeline/Configs/DatabaseConfig.cs b/Timeline/Configs/DatabaseConfig.cs index e24ecdfb..c9309b08 100644 --- a/Timeline/Configs/DatabaseConfig.cs +++ b/Timeline/Configs/DatabaseConfig.cs @@ -2,6 +2,6 @@ namespace Timeline.Configs {
public class DatabaseConfig
{
- public string ConnectionString { get; set; }
+ public string ConnectionString { get; set; } = default!;
}
}
diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs index 8c61d7bc..8a17825e 100644 --- a/Timeline/Configs/JwtConfig.cs +++ b/Timeline/Configs/JwtConfig.cs @@ -2,9 +2,9 @@ namespace Timeline.Configs {
public class JwtConfig
{
- public string Issuer { get; set; }
- public string Audience { get; set; }
- public string SigningKey { get; set; }
+ public string Issuer { get; set; } = default!;
+ public string Audience { get; set; } = default!;
+ public string SigningKey { get; set; } = default!;
/// <summary>
/// Set the default value of expire offset of jwt token.
diff --git a/Timeline/Controllers/UserTestController.cs b/Timeline/Controllers/Testing/TestingAuthController.cs index 2a5f36a1..67b5b2ef 100644 --- a/Timeline/Controllers/UserTestController.cs +++ b/Timeline/Controllers/Testing/TestingAuthController.cs @@ -1,12 +1,12 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Timeline.Authenticate;
+using Timeline.Authentication;
-namespace Timeline.Controllers
+namespace Timeline.Controllers.Testing
{
- [Route("Test/User")]
+ [Route("testing/auth")]
[ApiController]
- public class UserTestController : Controller
+ public class TestingAuthController : Controller
{
[HttpGet("[action]")]
[Authorize]
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 3c166448..4e32d26f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -3,71 +3,84 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
-using System.Collections.Generic;
using System.Threading.Tasks;
using Timeline.Models.Http;
using Timeline.Services;
-using static Timeline.Helpers.MyLogHelper;
+using Timeline.Helpers;
+using Microsoft.Extensions.Localization;
+using System.Globalization;
+using static Timeline.Resources.Controllers.TokenController;
-namespace Timeline.Controllers
+namespace Timeline
{
- [Route("token")]
- [ApiController]
- public class TokenController : Controller
+ public static partial class ErrorCodes
{
- private static class LoggingEventIds
- {
- public const int CreateSucceeded = 1000;
- public const int CreateFailed = 1001;
-
- public const int VerifySucceeded = 2000;
- public const int VerifyFailed = 2001;
- }
-
- public static class ErrorCodes
+ public static partial class Http
{
- public const int Create_UserNotExist = -1001;
- public const int Create_BadPassword = -1002;
- public const int Create_BadExpireOffset = -1003;
+ public static class Token // bbb = 001
+ {
+ public static class Create // cc = 01
+ {
+ public const int BadCredential = 10010101;
+ }
- public const int Verify_BadToken = -2001;
- public const int Verify_UserNotExist = -2002;
- public const int Verify_BadVersion = -2003;
- public const int Verify_Expired = -2004;
+ public static class Verify // cc = 02
+ {
+ public const int BadFormat = 10010201;
+ public const int UserNotExist = 10010202;
+ public const int OldVersion = 10010203;
+ public const int Expired = 10010204;
+ }
+ }
}
+ }
+}
+namespace Timeline.Controllers
+{
+ [Route("token")]
+ [ApiController]
+ public class TokenController : Controller
+ {
private readonly IUserService _userService;
private readonly ILogger<TokenController> _logger;
private readonly IClock _clock;
+ private readonly IStringLocalizer<TokenController> _localizer;
- public TokenController(IUserService userService, ILogger<TokenController> logger, IClock clock)
+ public TokenController(IUserService userService, ILogger<TokenController> logger, IClock clock, IStringLocalizer<TokenController> localizer)
{
_userService = userService;
_logger = logger;
_clock = clock;
+ _localizer = localizer;
}
[HttpPost("create")]
[AllowAnonymous]
- public async Task<IActionResult> Create([FromBody] CreateTokenRequest request)
+ public async Task<ActionResult<CreateTokenResponse>> Create([FromBody] CreateTokenRequest request)
{
- void LogFailure(string reason, int code, Exception e = null)
+ void LogFailure(string reason, Exception? e = null)
{
- _logger.LogInformation(LoggingEventIds.CreateFailed, e, FormatLogMessage("Attemp to login failed.",
- Pair("Reason", reason),
- Pair("Code", code),
- Pair("Username", request.Username),
- Pair("Password", request.Password),
- Pair("Expire Offset (in days)", request.ExpireOffset)));
+ _logger.LogInformation(e, Log.Format(LogCreateFailure,
+ ("Reason", reason),
+ ("Username", request.Username),
+ ("Password", request.Password),
+ ("Expire (in days)", request.Expire)
+ ));
}
try
{
- var expiredTime = request.ExpireOffset == null ? null : (DateTime?)(_clock.GetCurrentTime().AddDays(request.ExpireOffset.Value));
- var result = await _userService.CreateToken(request.Username, request.Password, expiredTime);
- _logger.LogInformation(LoggingEventIds.CreateSucceeded, FormatLogMessage("Attemp to login succeeded.",
- Pair("Username", request.Username),
- Pair("Expire Time", expiredTime == null ? "default" : expiredTime.Value.ToString())));
+ DateTime? expireTime = null;
+ if (request.Expire != null)
+ expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value);
+
+ var result = await _userService.CreateToken(request.Username, request.Password, expireTime);
+
+ _logger.LogInformation(Log.Format(LogCreateSuccess,
+ ("Username", request.Username),
+ ("Expire At", expireTime?.ToString(CultureInfo.CurrentUICulture.DateTimeFormat) ?? "default")
+ ));
return Ok(new CreateTokenResponse
{
Token = result.Token,
@@ -76,75 +89,71 @@ namespace Timeline.Controllers }
catch (UserNotExistException e)
{
- var code = ErrorCodes.Create_UserNotExist;
- LogFailure("User does not exist.", code, e);
- return BadRequest(new CommonResponse(code, "Bad username or password."));
+ LogFailure(LogUserNotExist, e);
+ return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential,
+ _localizer["ErrorBadCredential"]));
}
catch (BadPasswordException e)
{
- var code = ErrorCodes.Create_BadPassword;
- LogFailure("Password is wrong.", code, e);
- return BadRequest(new CommonResponse(code, "Bad username or password."));
+ LogFailure(LogBadPassword, e);
+ return BadRequest(new CommonResponse(ErrorCodes.Http.Token.Create.BadCredential,
+ _localizer["ErrorBadCredential"]));
}
}
[HttpPost("verify")]
[AllowAnonymous]
- public async Task<IActionResult> Verify([FromBody] VerifyTokenRequest request)
+ public async Task<ActionResult<VerifyTokenResponse>> Verify([FromBody] VerifyTokenRequest request)
{
- void LogFailure(string reason, int code, Exception e = null, params KeyValuePair<string, object>[] otherProperties)
+ void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties)
{
- var properties = new KeyValuePair<string, object>[3 + otherProperties.Length];
- properties[0] = Pair("Reason", reason);
- properties[1] = Pair("Code", code);
- properties[2] = Pair("Token", request.Token);
- otherProperties.CopyTo(properties, 3);
- _logger.LogInformation(LoggingEventIds.VerifyFailed, e, FormatLogMessage("Token verification failed.", properties));
+ var properties = new (string, object?)[2 + otherProperties.Length];
+ properties[0] = ("Reason", reason);
+ properties[1] = ("Token", request.Token);
+ otherProperties.CopyTo(properties, 2);
+ _logger.LogInformation(e, Log.Format(LogVerifyFailure, properties));
}
try
{
var result = await _userService.VerifyToken(request.Token);
- _logger.LogInformation(LoggingEventIds.VerifySucceeded,
- FormatLogMessage("Token verification succeeded.",
- Pair("Username", result.Username), Pair("Token", request.Token)));
+ _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)
{
- const string message = "Token is expired.";
- var code = ErrorCodes.Verify_Expired;
var innerException = e.InnerException as SecurityTokenExpiredException;
- LogFailure(message, code, e, Pair("Expires", innerException.Expires));
- return BadRequest(new CommonResponse(code, message));
+ 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
{
- const string message = "Token is of bad format.";
- var code = ErrorCodes.Verify_BadToken;
- LogFailure(message, code, e);
- return BadRequest(new CommonResponse(code, message));
+ LogFailure(LogVerifyBadFormat, e);
+ return BadRequest(new CommonResponse(
+ ErrorCodes.Http.Token.Verify.BadFormat, _localizer["ErrorVerifyBadFormat"]));
}
}
catch (UserNotExistException e)
{
- const string message = "User does not exist. Administrator might have deleted this user.";
- var code = ErrorCodes.Verify_UserNotExist;
- LogFailure(message, code, e);
- return BadRequest(new CommonResponse(code, message));
- }
- catch (BadTokenVersionException e)
- {
- const string message = "Token has a old version.";
- var code = ErrorCodes.Verify_BadVersion;
- LogFailure(message, code, e);
- _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because version is old. Code: {} Token: {}.", code, request.Token);
- return BadRequest(new CommonResponse(code, message));
+ LogFailure(LogVerifyUserNotExist, e);
+ return BadRequest(new CommonResponse(
+ ErrorCodes.Http.Token.Verify.UserNotExist, _localizer["ErrorVerifyUserNotExist"]));
}
}
}
diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index e77076ca..838a3928 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -1,68 +1,75 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using System;
using System.Linq;
using System.Threading.Tasks;
-using Timeline.Authenticate;
+using Timeline.Authentication;
using Timeline.Filters;
+using Timeline.Helpers;
using Timeline.Models.Http;
+using Timeline.Models.Validation;
using Timeline.Services;
-namespace Timeline.Controllers
+namespace Timeline
{
- [ApiController]
- public class UserAvatarController : Controller
+ public static partial class ErrorCodes
{
- public static class ErrorCodes
+ public static partial class Http
{
- public const int Get_UserNotExist = -1001;
-
- public const int Put_UserNotExist = -2001;
- public const int Put_Forbid = -2002;
- public const int Put_BadFormat_CantDecode = -2011;
- public const int Put_BadFormat_UnmatchedFormat = -2012;
- public const int Put_BadFormat_BadSize = -2013;
- public const int Put_Content_TooBig = -2021;
- public const int Put_Content_UnmatchedLength_Less = -2022;
- public const int Put_Content_UnmatchedLength_Bigger = -2023;
+ public static class UserAvatar // bbb = 003
+ {
+ public static class Get // cc = 01
+ {
+ public const int UserNotExist = 10030101;
+ }
- public const int Delete_UserNotExist = -3001;
- public const int Delete_Forbid = -3002;
+ public static class Put // cc = 02
+ {
+ public const int UserNotExist = 10030201;
+ public const int Forbid = 10030202;
+ public const int BadFormat_CantDecode = 10030203;
+ public const int BadFormat_UnmatchedFormat = 10030204;
+ public const int BadFormat_BadSize = 10030205;
+ }
- public static int From(AvatarDataException.ErrorReason error)
- {
- switch (error)
+ public static class Delete // cc = 03
{
- case AvatarDataException.ErrorReason.CantDecode:
- return Put_BadFormat_CantDecode;
- case AvatarDataException.ErrorReason.UnmatchedFormat:
- return Put_BadFormat_UnmatchedFormat;
- case AvatarDataException.ErrorReason.BadSize:
- return Put_BadFormat_BadSize;
- default:
- throw new Exception("Unknown AvatarDataException.ErrorReason value.");
+ public const int UserNotExist = 10030301;
+ public const int Forbid = 10030302;
}
}
}
+ }
+}
+namespace Timeline.Controllers
+{
+ [ApiController]
+ public class UserAvatarController : Controller
+ {
private readonly ILogger<UserAvatarController> _logger;
private readonly IUserAvatarService _service;
- public UserAvatarController(ILogger<UserAvatarController> logger, IUserAvatarService service)
+ private readonly IStringLocalizerFactory _localizerFactory;
+ private readonly IStringLocalizer<UserAvatarController> _localizer;
+
+ public UserAvatarController(ILogger<UserAvatarController> logger, IUserAvatarService service, IStringLocalizerFactory localizerFactory)
{
_logger = logger;
_service = service;
+ _localizerFactory = localizerFactory;
+ _localizer = new StringLocalizer<UserAvatarController>(localizerFactory);
}
[HttpGet("users/{username}/avatar")]
- [Authorize]
[ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)]
- public async Task<IActionResult> Get([FromRoute] string username)
+ public async Task<IActionResult> Get([FromRoute][Username] string username)
{
const string IfNonMatchHeaderKey = "If-None-Match";
@@ -74,11 +81,16 @@ namespace Timeline.Controllers if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
{
if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
- return BadRequest(CommonResponse.BadIfNonMatch());
+ {
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetBadIfNoneMatch,
+ ("Username", username), ("If-None-Match", value)));
+ return BadRequest(HeaderErrorResponse.BadIfNonMatch(_localizerFactory));
+ }
if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
{
Response.Headers.Add("ETag", eTagValue);
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnNotModify, ("Username", username)));
return StatusCode(StatusCodes.Status304NotModified);
}
}
@@ -86,12 +98,13 @@ namespace Timeline.Controllers var avatarInfo = await _service.GetAvatar(username);
var avatar = avatarInfo.Avatar;
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnData, ("Username", username)));
return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag);
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to get a avatar of a non-existent user failed. Username: {username} .");
- return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogGetUserNotExist, ("Username", username)));
+ return NotFound(new CommonResponse(ErrorCodes.Http.UserAvatar.Get.UserNotExist, _localizer["ErrorGetUserNotExist"]));
}
}
@@ -99,18 +112,18 @@ namespace Timeline.Controllers [Authorize]
[RequireContentType, RequireContentLength]
[Consumes("image/png", "image/jpeg", "image/gif", "image/webp")]
- public async Task<IActionResult> Put(string username)
+ public async Task<IActionResult> Put([FromRoute][Username] string username)
{
- var contentLength = Request.ContentLength.Value;
+ var contentLength = Request.ContentLength!.Value;
if (contentLength > 1000 * 1000 * 10)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig,
- "Content can't be bigger than 10MB."));
+ return BadRequest(ContentErrorResponse.TooBig(_localizerFactory, "10MB"));
- if (!User.IsAdmin() && User.Identity.Name != username)
+ if (!User.IsAdministrator() && User.Identity.Name != username)
{
- _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutForbid,
+ ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username)));
return StatusCode(StatusCodes.Status403Forbidden,
- new CommonResponse(ErrorCodes.Put_Forbid, "Normal user can't change other's avatar."));
+ new CommonResponse(ErrorCodes.Http.UserAvatar.Put.Forbid, _localizer["ErrorPutForbid"]));
}
try
@@ -119,13 +132,11 @@ namespace Timeline.Controllers var bytesRead = await Request.Body.ReadAsync(data);
if (bytesRead != contentLength)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Less,
- $"Content length in header is {contentLength} but actual length is {bytesRead}."));
+ return BadRequest(ContentErrorResponse.UnmatchedLength_Smaller(_localizerFactory));
var extraByte = new byte[1];
if (await Request.Body.ReadAsync(extraByte) != 0)
- return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Bigger,
- $"Content length in header is {contentLength} but actual length is bigger than that."));
+ return BadRequest(ContentErrorResponse.UnmatchedLength_Bigger(_localizerFactory));
await _service.SetAvatar(username, new Avatar
{
@@ -133,43 +144,57 @@ namespace Timeline.Controllers Type = Request.ContentType
});
- _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutSuccess,
+ ("Username", username), ("Mime Type", Request.ContentType)));
return Ok();
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to put a avatar of a non-existent user failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserNotExist, ("Username", username)));
+ return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Put.UserNotExist, _localizer["ErrorPutUserNotExist"]));
}
- catch (AvatarDataException e)
+ catch (AvatarFormatException e)
{
- _logger.LogInformation(e, $"Attempt to put a avatar of a bad format failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.From(e.Error), "Bad format."));
+ var (code, message) = e.Error switch
+ {
+ AvatarFormatException.ErrorReason.CantDecode =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_CantDecode, _localizer["ErrorPutBadFormatCantDecode"]),
+ AvatarFormatException.ErrorReason.UnmatchedFormat =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_UnmatchedFormat, _localizer["ErrorPutBadFormatUnmatchedFormat"]),
+ AvatarFormatException.ErrorReason.BadSize =>
+ (ErrorCodes.Http.UserAvatar.Put.BadFormat_BadSize, _localizer["ErrorPutBadFormatBadSize"]),
+ _ =>
+ throw new Exception(Resources.Controllers.UserAvatarController.ExceptionUnknownAvatarFormatError)
+ };
+
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, ("Username", username)));
+ return BadRequest(new CommonResponse(code, message));
}
}
[HttpDelete("users/{username}/avatar")]
[Authorize]
- public async Task<IActionResult> Delete([FromRoute] string username)
+ public async Task<IActionResult> Delete([FromRoute][Username] string username)
{
- if (!User.IsAdmin() && User.Identity.Name != username)
+ if (!User.IsAdministrator() && User.Identity.Name != username)
{
- _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat,
+ ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username)));
return StatusCode(StatusCodes.Status403Forbidden,
- new CommonResponse(ErrorCodes.Delete_Forbid, "Normal user can't delete other's avatar."));
+ new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.Forbid, _localizer["ErrorDeleteForbid"]));
}
try
{
await _service.SetAvatar(username, null);
- _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} .");
+ _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogDeleteSuccess, ("Username", username)));
return Ok();
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, $"Attempt to delete a avatar of a non-existent user failed. Username: {username} .");
- return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist."));
+ _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogDeleteNotExist, ("Username", username)));
+ return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.UserNotExist, _localizer["ErrorDeleteUserNotExist"]));
}
}
}
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index bd13f0a3..1771dc85 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,40 +1,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
-using System;
using System.Threading.Tasks;
-using Timeline.Authenticate;
+using Timeline.Authentication;
+using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Http;
+using Timeline.Models.Validation;
using Timeline.Services;
-using static Timeline.Helpers.MyLogHelper;
+using static Timeline.Resources.Controllers.UserController;
-namespace Timeline.Controllers
+namespace Timeline
{
- [ApiController]
- public class UserController : Controller
+ public static partial class ErrorCodes
{
- public static class ErrorCodes
+ public static partial class Http
{
- public const int Get_NotExist = -1001;
+ public static class User // bbb = 002
+ {
+ public static class Get // cc = 01
+ {
+ public const int NotExist = 10020101; // dd = 01
+ }
- public const int Put_BadUsername = -2001;
+ public static class Patch // cc = 03
+ {
+ public const int NotExist = 10020301; // dd = 01
+ }
- public const int Patch_NotExist = -3001;
+ public static class Op // cc = 1x
+ {
+ public static class ChangeUsername // cc = 11
+ {
+ public const int NotExist = 10021101; // dd = 01
+ public const int AlreadyExist = 10021102; // dd = 02
+ }
- public const int ChangeUsername_NotExist = -4001;
- public const int ChangeUsername_AlreadyExist = -4002;
+ public static class ChangePassword // cc = 12
+ {
+ public const int BadOldPassword = 10021201; // dd = 01
+ }
+ }
- public const int ChangePassword_BadOldPassword = -5001;
+ }
}
+ }
+}
+
+namespace Timeline.Controllers
+{
+ [ApiController]
+ public class UserController : Controller
+ {
private readonly ILogger<UserController> _logger;
private readonly IUserService _userService;
+ private readonly IStringLocalizerFactory _localizerFactory;
+ private readonly IStringLocalizer _localizer;
- public UserController(ILogger<UserController> logger, IUserService userService)
+ public UserController(ILogger<UserController> logger, IUserService userService, IStringLocalizerFactory localizerFactory)
{
_logger = logger;
_userService = userService;
+ _localizerFactory = localizerFactory;
+ _localizer = localizerFactory.Create(GetType());
}
[HttpGet("users"), AdminAuthorize]
@@ -44,44 +74,36 @@ namespace Timeline.Controllers }
[HttpGet("users/{username}"), AdminAuthorize]
- public async Task<IActionResult> Get([FromRoute] string username)
+ public async Task<ActionResult<UserInfo>> Get([FromRoute][Username] string username)
{
var user = await _userService.GetUser(username);
if (user == null)
{
- _logger.LogInformation(FormatLogMessage("Attempt to get a non-existent user.", Pair("Username", username)));
- return NotFound(new CommonResponse(ErrorCodes.Get_NotExist, "The user does not exist."));
+ _logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username)));
+ return NotFound(new CommonResponse(ErrorCodes.Http.User.Get.NotExist, _localizer["ErrorGetUserNotExist"]));
}
return Ok(user);
}
[HttpPut("users/{username}"), AdminAuthorize]
- public async Task<IActionResult> Put([FromBody] UserPutRequest request, [FromRoute] string username)
+ public async Task<ActionResult<CommonPutResponse>> Put([FromBody] UserPutRequest request, [FromRoute][Username] string username)
{
- try
- {
- var result = await _userService.PutUser(username, request.Password, request.Administrator.Value);
- switch (result)
- {
- case PutResult.Created:
- _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username)));
- return CreatedAtAction("Get", new { username }, CommonPutResponse.Created);
- case PutResult.Modified:
- _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username)));
- return Ok(CommonPutResponse.Modified);
- default:
- throw new Exception("Unreachable code.");
- }
- }
- catch (UsernameBadFormatException e)
+ var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value);
+ switch (result)
{
- _logger.LogInformation(e, FormatLogMessage("Attempt to create a user with bad username failed.", Pair("Username", username)));
- return BadRequest(new CommonResponse(ErrorCodes.Put_BadUsername, "Username is of bad format."));
+ case PutResult.Create:
+ _logger.LogInformation(Log.Format(LogPutCreate, ("Username", username)));
+ return CreatedAtAction("Get", new { username }, CommonPutResponse.Create(_localizerFactory));
+ case PutResult.Modify:
+ _logger.LogInformation(Log.Format(LogPutModify, ("Username", username)));
+ return Ok(CommonPutResponse.Modify(_localizerFactory));
+ default:
+ throw new InvalidBranchException();
}
}
[HttpPatch("users/{username}"), AdminAuthorize]
- public async Task<IActionResult> Patch([FromBody] UserPatchRequest request, [FromRoute] string username)
+ public async Task<ActionResult> Patch([FromBody] UserPatchRequest request, [FromRoute][Username] string username)
{
try
{
@@ -90,66 +112,67 @@ namespace Timeline.Controllers }
catch (UserNotExistException e)
{
- _logger.LogInformation(e, FormatLogMessage("Attempt to patch a non-existent user.", Pair("Username", username)));
- return NotFound(new CommonResponse(ErrorCodes.Patch_NotExist, "The user does not exist."));
+ _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username)));
+ return NotFound(new CommonResponse(ErrorCodes.Http.User.Patch.NotExist, _localizer["ErrorPatchUserNotExist"]));
}
}
[HttpDelete("users/{username}"), AdminAuthorize]
- public async Task<IActionResult> Delete([FromRoute] string username)
+ public async Task<ActionResult<CommonDeleteResponse>> Delete([FromRoute][Username] string username)
{
try
{
await _userService.DeleteUser(username);
- _logger.LogInformation(FormatLogMessage("A user is deleted.", Pair("Username", username)));
- return Ok(CommonDeleteResponse.Deleted);
+ _logger.LogInformation(Log.Format(LogDeleteDelete, ("Username", username)));
+ return Ok(CommonDeleteResponse.Delete(_localizerFactory));
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, FormatLogMessage("Attempt to delete a non-existent user.", Pair("Username", username)));
- return Ok(CommonDeleteResponse.NotExists);
+ _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username)));
+ return Ok(CommonDeleteResponse.NotExist(_localizerFactory));
}
}
[HttpPost("userop/changeusername"), AdminAuthorize]
- public async Task<IActionResult> ChangeUsername([FromBody] ChangeUsernameRequest request)
+ public async Task<ActionResult> ChangeUsername([FromBody] ChangeUsernameRequest request)
{
try
{
await _userService.ChangeUsername(request.OldUsername, request.NewUsername);
- _logger.LogInformation(FormatLogMessage("A user changed username.",
- Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername)));
+ _logger.LogInformation(Log.Format(LogChangeUsernameSuccess,
+ ("Old Username", request.OldUsername), ("New Username", request.NewUsername)));
return Ok();
}
catch (UserNotExistException e)
{
- _logger.LogInformation(e, FormatLogMessage("Attempt to change a non-existent user's username failed.",
- Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername)));
- return BadRequest(new CommonResponse(ErrorCodes.ChangeUsername_NotExist, $"The user {request.OldUsername} does not exist."));
+ _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, FormatLogMessage("Attempt to change a user's username to a existent one failed.",
- Pair("Old Username", request.OldUsername), Pair("New Username", request.NewUsername)));
- return BadRequest(new CommonResponse(ErrorCodes.ChangeUsername_AlreadyExist, $"The user {request.NewUsername} already exists."));
+ _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"]));
}
// there is no need to catch bad format exception because it is already checked in model validation.
}
[HttpPost("userop/changepassword"), Authorize]
- public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
+ public async Task<ActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
try
{
- await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword);
- _logger.LogInformation(FormatLogMessage("A user changed password.", Pair("Username", User.Identity.Name)));
+ await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword);
+ _logger.LogInformation(Log.Format(LogChangePasswordSuccess, ("Username", User.Identity.Name)));
return Ok();
}
catch (BadPasswordException e)
{
- _logger.LogInformation(e, FormatLogMessage("A user attempt to change password but old password is wrong.",
- Pair("Username", User.Identity.Name), Pair("Old Password", request.OldPassword)));
- return BadRequest(new CommonResponse(ErrorCodes.ChangePassword_BadOldPassword, "Old password is wrong."));
+ _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"]));
}
// User can't be non-existent or the token is bad.
}
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/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index d9815660..e1b98e7d 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -1,38 +1,7 @@ using Microsoft.EntityFrameworkCore;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
namespace Timeline.Entities
{
- public static class UserRoles
- {
- public const string Admin = "admin";
- public const string User = "user";
- }
-
- [Table("users")]
- public class User
- {
- [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public long Id { get; set; }
-
- [Column("name"), MaxLength(26), Required]
- public string Name { get; set; }
-
- [Column("password"), Required]
- public string EncryptedPassword { get; set; }
-
- [Column("roles"), Required]
- public string RoleString { get; set; }
-
- [Column("version"), Required]
- public long Version { get; set; }
-
- public UserAvatar Avatar { get; set; }
-
- public UserDetailEntity Detail { get; set; }
- }
-
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options)
@@ -41,14 +10,14 @@ namespace Timeline.Entities }
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Version).HasDefaultValue(0);
modelBuilder.Entity<User>().HasIndex(e => e.Name).IsUnique();
}
- public DbSet<User> Users { get; set; }
- public DbSet<UserAvatar> UserAvatars { get; set; }
- public DbSet<UserDetailEntity> UserDetails { get; set; }
+ public DbSet<User> Users { get; set; } = default!;
+ public DbSet<UserAvatar> UserAvatars { get; set; } = default!;
}
}
diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs new file mode 100644 index 00000000..6e8e4967 --- /dev/null +++ b/Timeline/Entities/User.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ public static class UserRoles
+ {
+ public const string Admin = "admin";
+ public const string User = "user";
+ }
+
+ [Table("users")]
+ public class User
+ {
+ [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("name"), MaxLength(26), Required]
+ public string Name { get; set; } = default!;
+
+ [Column("password"), Required]
+ public string EncryptedPassword { get; set; } = default!;
+
+ [Column("roles"), Required]
+ public string RoleString { get; set; } = default!;
+
+ [Column("version"), Required]
+ public long Version { get; set; }
+
+ public UserAvatar? Avatar { get; set; }
+ }
+}
diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index d549aea5..a5b18b94 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -11,29 +11,18 @@ namespace Timeline.Entities public long Id { get; set; }
[Column("data")]
- public byte[] Data { get; set; }
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
+ public byte[]? Data { get; set; }
[Column("type")]
- public string Type { get; set; }
+ public string? Type { get; set; }
[Column("etag"), MaxLength(30)]
- public string ETag { get; set; }
+ public string? ETag { get; set; }
[Column("last_modified"), Required]
public DateTime LastModified { get; set; }
public long UserId { get; set; }
-
- public static UserAvatar Create(DateTime lastModified)
- {
- return new UserAvatar
- {
- Id = 0,
- Data = null,
- Type = null,
- ETag = null,
- LastModified = lastModified
- };
- }
}
}
diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs deleted file mode 100644 index bc14dbe6..00000000 --- a/Timeline/Entities/UserDetail.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Timeline.Entities
-{
- [Table("user_details")]
- public class UserDetailEntity
- {
- [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public long Id { get; set; }
-
- [Column("nickname"), MaxLength(15)]
- public string Nickname { get; set; }
-
- [Column("qq"), MaxLength(15)]
- public string QQ { get; set; }
-
- [Column("email"), MaxLength(50)]
- public string Email { get; set; }
-
- [Column("phone_number"), MaxLength(15)]
- public string PhoneNumber { get; set; }
-
- [Column("description")]
- public string Description { get; set; }
-
- public long UserId { get; set; }
- }
-}
diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs new file mode 100644 index 00000000..5e7f003a --- /dev/null +++ b/Timeline/ErrorCodes.cs @@ -0,0 +1,36 @@ +namespace Timeline
+{
+ /// <summary>
+ /// All error code constants.
+ /// </summary>
+ /// <remarks>
+ /// Scheme:
+ /// abbbccdd
+ /// </remarks>
+ public static partial class ErrorCodes
+ {
+ public static partial class Http // a = 1
+ {
+ public static class Common // bbb = 000
+ {
+ public const int InvalidModel = 10000000;
+
+ public static class Header // cc = 01
+ {
+ public const int Missing_ContentType = 10000101; // dd = 01
+ public const int Missing_ContentLength = 10000102; // dd = 02
+ public const int Zero_ContentLength = 10000103; // dd = 03
+ public const int BadFormat_IfNonMatch = 10000104; // dd = 04
+ }
+
+ public static class Content // cc = 02
+ {
+ public const int TooBig = 1000201;
+ public const int UnmatchedLength_Smaller = 10030202;
+ public const int UnmatchedLength_Bigger = 10030203;
+ }
+ }
+ }
+
+ }
+}
diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs index 14685a01..e3d4eeb2 100644 --- a/Timeline/Filters/ContentHeaderAttributes.cs +++ b/Timeline/Filters/ContentHeaderAttributes.cs @@ -1,16 +1,20 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
using Timeline.Models.Http;
namespace Timeline.Filters
{
public class RequireContentTypeAttribute : ActionFilterAttribute
{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentType == null)
{
- context.Result = new BadRequestObjectResult(CommonResponse.MissingContentType());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentType(localizerFactory));
}
}
}
@@ -30,17 +34,20 @@ namespace Timeline.Filters public bool RequireNonZero { get; set; }
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.ContentLength == null)
{
- context.Result = new BadRequestObjectResult(CommonResponse.MissingContentLength());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentLength(localizerFactory));
return;
}
if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0)
{
- context.Result = new BadRequestObjectResult(CommonResponse.ZeroContentLength());
+ var localizerFactory = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizerFactory>();
+ context.Result = new BadRequestObjectResult(HeaderErrorResponse.ZeroContentLength(localizerFactory));
return;
}
}
diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs new file mode 100644 index 00000000..e720e5b7 --- /dev/null +++ b/Timeline/GlobalSuppressions.cs @@ -0,0 +1,13 @@ +// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "This is not bad.")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need to check the null because it's ASP.Net's duty.", Scope = "namespaceanddescendants", Target = "Timeline.Controllers")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Migrations code are auto generated.", Scope = "namespaceanddescendants", Target = "Timeline.Migrations")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Error code constant identifiers.", Scope = "type", Target = "Timeline.ErrorCodes")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1824:Mark assemblies with NeutralResourcesLanguageAttribute", Justification = "Applying this breaks the function of resources in my app.")]
diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index c792e845..643c99ac 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -6,6 +6,7 @@ namespace Timeline.Helpers {
public static class InvalidModelResponseFactory
{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
public static IActionResult Factory(ActionContext context)
{
var modelState = context.ModelState;
diff --git a/Timeline/Helpers/LanguageHelper.cs b/Timeline/Helpers/LanguageHelper.cs new file mode 100644 index 00000000..b0156b8b --- /dev/null +++ b/Timeline/Helpers/LanguageHelper.cs @@ -0,0 +1,12 @@ +using System.Linq;
+
+namespace Timeline.Helpers
+{
+ public static class LanguageHelper
+ {
+ public static bool AreSame(this bool firstBool, params bool[] otherBools)
+ {
+ return otherBools.All(b => b == firstBool);
+ }
+ }
+}
diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 123e8a8e..68c975fa 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -3,20 +3,19 @@ using System.Text; namespace Timeline.Helpers
{
- public static class MyLogHelper
+ public static class Log
{
- public static KeyValuePair<string, object> Pair(string key, object value) => new KeyValuePair<string, object>(key, value);
-
- public static string FormatLogMessage(string summary, params KeyValuePair<string, object>[] properties)
+ public static string Format(string summary, params (string, object?)[] properties)
{
var builder = new StringBuilder();
builder.Append(summary);
foreach (var property in properties)
{
+ var (key, value) = property;
builder.AppendLine();
- builder.Append(property.Key);
+ builder.Append(key);
builder.Append(" : ");
- builder.Append(property.Value);
+ builder.Append(value);
}
return builder.ToString();
}
diff --git a/Timeline/Helpers/StringLocalizerFactoryExtensions.cs b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs new file mode 100644 index 00000000..c2252b2c --- /dev/null +++ b/Timeline/Helpers/StringLocalizerFactoryExtensions.cs @@ -0,0 +1,19 @@ +
+using Microsoft.Extensions.Localization;
+using System.Reflection;
+
+namespace Timeline.Helpers
+{
+ internal static class StringLocalizerFactoryExtensions
+ {
+ internal static IStringLocalizer Create(this IStringLocalizerFactory factory, string basename)
+ {
+ return factory.Create(basename, new AssemblyName(typeof(StringLocalizerFactoryExtensions).Assembly.FullName!).Name);
+ }
+
+ internal static StringLocalizer<T> Create<T>(this IStringLocalizerFactory factory)
+ {
+ return new StringLocalizer<T>(factory);
+ }
+ }
+}
\ No newline at end of file diff --git a/Timeline/InvalidBranchException.cs b/Timeline/InvalidBranchException.cs new file mode 100644 index 00000000..32937c5d --- /dev/null +++ b/Timeline/InvalidBranchException.cs @@ -0,0 +1,16 @@ +using System;
+
+namespace Timeline
+{
+
+ [Serializable]
+ public class InvalidBranchException : Exception
+ {
+ public InvalidBranchException() : base(Resources.Common.ExceptionInvalidBranch) { }
+ public InvalidBranchException(string message) : base(message) { }
+ public InvalidBranchException(string message, Exception inner) : base(message, inner) { }
+ protected InvalidBranchException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs b/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs deleted file mode 100644 index 2bbcf673..00000000 --- a/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs +++ /dev/null @@ -1,136 +0,0 @@ -// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Timeline.Entities;
-
-namespace Timeline.Migrations
-{
- [DbContext(typeof(DatabaseContext))]
- [Migration("20190822072156_AddUserDetail")]
- partial class AddUserDetail
- {
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
- .HasAnnotation("Relational:MaxIdentifierLength", 64);
-
- modelBuilder.Entity("Timeline.Entities.User", b =>
- {
- b.Property<long>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnName("id");
-
- b.Property<string>("EncryptedPassword")
- .IsRequired()
- .HasColumnName("password");
-
- b.Property<string>("Name")
- .IsRequired()
- .HasColumnName("name")
- .HasMaxLength(26);
-
- b.Property<string>("RoleString")
- .IsRequired()
- .HasColumnName("roles");
-
- b.Property<long>("Version")
- .ValueGeneratedOnAdd()
- .HasColumnName("version")
- .HasDefaultValue(0L);
-
- b.HasKey("Id");
-
- b.HasIndex("Name")
- .IsUnique();
-
- b.ToTable("users");
- });
-
- modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
- {
- b.Property<long>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnName("id");
-
- b.Property<byte[]>("Data")
- .HasColumnName("data");
-
- b.Property<string>("ETag")
- .HasColumnName("etag")
- .HasMaxLength(30);
-
- b.Property<DateTime>("LastModified")
- .HasColumnName("last_modified");
-
- b.Property<string>("Type")
- .HasColumnName("type");
-
- b.Property<long>("UserId");
-
- b.HasKey("Id");
-
- b.HasIndex("UserId")
- .IsUnique();
-
- b.ToTable("user_avatars");
- });
-
- modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
- {
- b.Property<long>("Id")
- .ValueGeneratedOnAdd()
- .HasColumnName("id");
-
- b.Property<string>("Description")
- .HasColumnName("description");
-
- b.Property<string>("EMail")
- .HasColumnName("email")
- .HasMaxLength(50);
-
- b.Property<string>("Nickname")
- .HasColumnName("nickname")
- .HasMaxLength(15);
-
- b.Property<string>("PhoneNumber")
- .HasColumnName("phone_number")
- .HasMaxLength(15);
-
- b.Property<string>("QQ")
- .HasColumnName("qq")
- .HasMaxLength(15);
-
- b.Property<long>("UserId");
-
- b.HasKey("Id");
-
- b.HasIndex("UserId")
- .IsUnique();
-
- b.ToTable("user_details");
- });
-
- modelBuilder.Entity("Timeline.Entities.UserAvatar", b =>
- {
- b.HasOne("Timeline.Entities.User")
- .WithOne("Avatar")
- .HasForeignKey("Timeline.Entities.UserAvatar", "UserId")
- .OnDelete(DeleteBehavior.Cascade);
- });
-
- modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b =>
- {
- b.HasOne("Timeline.Entities.User")
- .WithOne("Detail")
- .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId")
- .OnDelete(DeleteBehavior.Cascade);
- });
-#pragma warning restore 612, 618
- }
- }
-}
diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.cs b/Timeline/Migrations/20190822072156_AddUserDetail.cs deleted file mode 100644 index 4aa6446b..00000000 --- a/Timeline/Migrations/20190822072156_AddUserDetail.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-namespace Timeline.Migrations
-{
- public partial class AddUserDetail : Migration
- {
- protected override void Up(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.CreateTable(
- name: "user_details",
- columns: table => new
- {
- id = table.Column<long>(nullable: false)
- .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
- nickname = table.Column<string>(maxLength: 15, nullable: true),
- qq = table.Column<string>(maxLength: 15, nullable: true),
- email = table.Column<string>(maxLength: 50, nullable: true),
- phone_number = table.Column<string>(maxLength: 15, nullable: true),
- description = table.Column<string>(nullable: true),
- UserId = table.Column<long>(nullable: false)
- },
- constraints: table =>
- {
- table.PrimaryKey("PK_user_details", x => x.id);
- table.ForeignKey(
- name: "FK_user_details_users_UserId",
- column: x => x.UserId,
- principalTable: "users",
- principalColumn: "id",
- onDelete: ReferentialAction.Cascade);
- });
-
- migrationBuilder.CreateIndex(
- name: "IX_user_details_UserId",
- table: "user_details",
- column: "UserId",
- unique: true);
- }
-
- protected override void Down(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.DropTable(
- name: "user_details");
- }
- }
-}
diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index a72f187c..39ddddd9 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -1,76 +1,164 @@ +using Microsoft.Extensions.Localization;
+using Timeline.Helpers;
+
namespace Timeline.Models.Http
{
public class CommonResponse
{
- public static class ErrorCodes
+ internal static CommonResponse InvalidModel(string message)
+ {
+ return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message);
+ }
+
+ public CommonResponse()
{
- /// <summary>
- /// Used when the model is invaid.
- /// For example a required field is null.
- /// </summary>
- public const int InvalidModel = -100;
- public const int Header_Missing_ContentType = -111;
- public const int Header_Missing_ContentLength = -112;
- public const int Header_Zero_ContentLength = -113;
- public const int Header_BadFormat_IfNonMatch = -114;
}
- public static CommonResponse InvalidModel(string message)
+ public CommonResponse(int code, string message)
{
- return new CommonResponse(ErrorCodes.InvalidModel, message);
+ Code = code;
+ Message = message;
}
- public static CommonResponse MissingContentType()
+ public int Code { get; set; }
+ public string? Message { get; set; }
+ }
+
+ internal static class HeaderErrorResponse
+ {
+ internal static CommonResponse MissingContentType(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Header_Missing_ContentType, "Header Content-Type is required.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, localizer["HeaderMissingContentType"]);
}
- public static CommonResponse MissingContentLength()
+ internal static CommonResponse MissingContentLength(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Header_Missing_ContentLength, "Header Content-Length is missing or of bad format.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, localizer["HeaderMissingContentLength"]);
}
- public static CommonResponse ZeroContentLength()
+ internal static CommonResponse ZeroContentLength(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Header_Zero_ContentLength, "Header Content-Length must not be 0.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, localizer["HeaderZeroContentLength"]);
}
- public static CommonResponse BadIfNonMatch()
+ internal static CommonResponse BadIfNonMatch(IStringLocalizerFactory localizerFactory)
{
- return new CommonResponse(ErrorCodes.Header_BadFormat_IfNonMatch, "Header If-Non-Match is of bad format.");
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, localizer["HeaderBadIfNonMatch"]);
}
+ }
- public CommonResponse()
+ internal static class ContentErrorResponse
+ {
+ internal static CommonResponse TooBig(IStringLocalizerFactory localizerFactory, string maxLength)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.TooBig, localizer["ContentTooBig", maxLength]);
+ }
+
+ internal static CommonResponse UnmatchedLength_Smaller(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Smaller, localizer["ContentUnmatchedLengthSmaller"]);
+ }
+ internal static CommonResponse UnmatchedLength_Bigger(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Bigger, localizer["ContentUnmatchedLengthBigger"]);
+ }
+ }
+
+
+ public class CommonDataResponse<T> : CommonResponse
+ {
+ public CommonDataResponse()
{
}
- public CommonResponse(int code, string message)
+ public CommonDataResponse(int code, string message, T data)
+ : base(code, message)
{
- Code = code;
- Message = message;
+ Data = data;
}
- public int Code { get; set; }
- public string Message { get; set; }
+ public T Data { get; set; } = default!;
}
- public static class CommonPutResponse
+ public class CommonPutResponse : CommonDataResponse<CommonPutResponse.ResponseData>
{
- public const int CreatedCode = 0;
- public const int ModifiedCode = 1;
+ public class ResponseData
+ {
+ public ResponseData(bool create)
+ {
+ Create = create;
+ }
- public static CommonResponse Created { get; } = new CommonResponse(CreatedCode, "A new item is created.");
- public static CommonResponse Modified { get; } = new CommonResponse(ModifiedCode, "An existent item is modified.");
+ public bool Create { get; set; }
+ }
+
+ public CommonPutResponse()
+ {
+
+ }
+
+ public CommonPutResponse(int code, string message, bool create)
+ : base(code, message, new ResponseData(create))
+ {
+
+ }
+
+ internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonPutResponse(0, localizer["PutCreate"], true);
+ }
+
+ internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonPutResponse(0, localizer["PutModify"], false);
+
+ }
}
- public static class CommonDeleteResponse
+ public class CommonDeleteResponse : CommonDataResponse<CommonDeleteResponse.ResponseData>
{
- public const int DeletedCode = 0;
- public const int NotExistsCode = 1;
+ public class ResponseData
+ {
+ public ResponseData(bool delete)
+ {
+ Delete = delete;
+ }
- public static CommonResponse Deleted { get; } = new CommonResponse(DeletedCode, "An existent item is deleted.");
- public static CommonResponse NotExists { get; } = new CommonResponse(NotExistsCode, "The item does not exist.");
+ public bool Delete { get; set; }
+ }
+
+ public CommonDeleteResponse()
+ {
+
+ }
+
+ public CommonDeleteResponse(int code, string message, bool delete)
+ : base(code, message, new ResponseData(delete))
+ {
+
+ }
+
+ internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Http.Common");
+ return new CommonDeleteResponse(0, localizer["DeleteDelete"], true);
+ }
+
+ internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory)
+ {
+ var localizer = localizerFactory.Create("Models.Models.Http.Common");
+ return new CommonDeleteResponse(0, localizer["DeleteNotExist"], false);
+ }
}
}
diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs index 68a66d0a..ea8b59ed 100644 --- a/Timeline/Models/Http/Token.cs +++ b/Timeline/Models/Http/Token.cs @@ -5,28 +5,28 @@ namespace Timeline.Models.Http public class CreateTokenRequest
{
[Required]
- public string Username { get; set; }
+ public string Username { get; set; } = default!;
[Required]
- public string Password { get; set; }
+ public string Password { get; set; } = default!;
// in days, optional
[Range(1, 365)]
- public int? ExpireOffset { get; set; }
+ public int? Expire { get; set; }
}
public class CreateTokenResponse
{
- public string Token { get; set; }
- public UserInfo User { get; set; }
+ public string Token { get; set; } = default!;
+ public UserInfo User { get; set; } = default!;
}
public class VerifyTokenRequest
{
[Required]
- public string Token { get; set; }
+ public string Token { get; set; } = default!;
}
public class VerifyTokenResponse
{
- public UserInfo User { get; set; }
+ public UserInfo User { get; set; } = default!;
}
}
diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 4308a19c..516c1329 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -6,31 +6,33 @@ namespace Timeline.Models.Http public class UserPutRequest
{
[Required]
- public string Password { get; set; }
+ public string Password { get; set; } = default!;
[Required]
public bool? Administrator { get; set; }
}
public class UserPatchRequest
{
- public string Password { get; set; }
+ public string? Password { get; set; }
public bool? Administrator { get; set; }
}
public class ChangeUsernameRequest
{
[Required]
- public string OldUsername { get; set; }
+ [Username]
+ public string OldUsername { get; set; } = default!;
- [Required, ValidateWith(typeof(UsernameValidator))]
- public string NewUsername { get; set; }
+ [Required]
+ [Username]
+ public string NewUsername { get; set; } = default!;
}
public class ChangePasswordRequest
{
[Required]
- public string OldPassword { get; set; }
+ public string OldPassword { get; set; } = default!;
[Required]
- public string NewPassword { get; set; }
+ public string NewPassword { get; set; } = default!;
}
}
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/UserConvert.cs b/Timeline/Models/UserConvert.cs new file mode 100644 index 00000000..5b132421 --- /dev/null +++ b/Timeline/Models/UserConvert.cs @@ -0,0 +1,67 @@ +using System;
+using System.Collections.Generic;
+using System.Linq;
+using Timeline.Entities;
+using Timeline.Services;
+
+namespace Timeline.Models
+{
+ public static class UserConvert
+ {
+ public static UserInfo CreateUserInfo(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ return new UserInfo(user.Name, UserRoleConvert.ToBool(user.RoleString));
+ }
+
+ internal static UserCache CreateUserCache(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ return new UserCache
+ {
+ Username = user.Name,
+ Administrator = UserRoleConvert.ToBool(user.RoleString),
+ Version = user.Version
+ };
+ }
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")]
+ public static class UserRoleConvert
+ {
+ public const string UserRole = UserRoles.User;
+ public const string AdminRole = UserRoles.Admin;
+
+ public static string[] ToArray(bool administrator)
+ {
+ return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole };
+ }
+
+ public static string[] ToArray(string s)
+ {
+ return s.Split(',').ToArray();
+ }
+
+ public static bool ToBool(IReadOnlyCollection<string> roles)
+ {
+ return roles.Contains(AdminRole);
+ }
+
+ public static string ToString(IReadOnlyCollection<string> roles)
+ {
+ return string.Join(',', roles);
+ }
+
+ public static string ToString(bool administrator)
+ {
+ return administrator ? UserRole + "," + AdminRole : UserRole;
+ }
+
+ public static bool ToBool(string s)
+ {
+ return s.Contains("admin", StringComparison.InvariantCulture);
+ }
+ }
+}
diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs deleted file mode 100644 index 1a6c0c6a..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/UserInfo.cs b/Timeline/Models/UserInfo.cs index e502855b..b60bdfa2 100644 --- a/Timeline/Models/UserInfo.cs +++ b/Timeline/Models/UserInfo.cs @@ -12,8 +12,8 @@ namespace Timeline.Models Administrator = administrator;
}
- public string Username { get; set; }
- public bool Administrator { get; set; }
+ public string Username { get; set; } = default!;
+ public bool Administrator { get; set; } = default!;
public override string ToString()
{
diff --git a/Timeline/Models/UserUtility.cs b/Timeline/Models/UserUtility.cs deleted file mode 100644 index 405987b5..00000000 --- a/Timeline/Models/UserUtility.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System;
-using System.Linq;
-using Timeline.Entities;
-using Timeline.Services;
-
-namespace Timeline.Models
-{
- public static class UserUtility
- {
- public const string UserRole = UserRoles.User;
- public const string AdminRole = UserRoles.Admin;
-
- public static string[] UserRoleArray { get; } = new string[] { UserRole };
- public static string[] AdminRoleArray { get; } = new string[] { UserRole, AdminRole };
-
- public static string[] IsAdminToRoleArray(bool isAdmin)
- {
- return isAdmin ? AdminRoleArray : UserRoleArray;
- }
-
- public static bool RoleArrayToIsAdmin(string[] roles)
- {
- return roles.Contains(AdminRole);
- }
-
- public static string[] RoleStringToRoleArray(string roleString)
- {
- return roleString.Split(',').ToArray();
- }
-
- public static string RoleArrayToRoleString(string[] roles)
- {
- return string.Join(',', roles);
- }
-
- public static string IsAdminToRoleString(bool isAdmin)
- {
- return RoleArrayToRoleString(IsAdminToRoleArray(isAdmin));
- }
-
- public static bool RoleStringToIsAdmin(string roleString)
- {
- return RoleArrayToIsAdmin(RoleStringToRoleArray(roleString));
- }
-
- public static UserInfo CreateUserInfo(User user)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
- return new UserInfo(user.Name, RoleStringToIsAdmin(user.RoleString));
- }
-
- internal static UserCache CreateUserCache(User user)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
- return new UserCache { Username = user.Name, Administrator = RoleStringToIsAdmin(user.RoleString), Version = user.Version };
- }
- }
-}
diff --git a/Timeline/Models/Validation/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..dc237add 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -1,45 +1,51 @@ -using System.Linq;
-using System.Text.RegularExpressions;
+using System;
+using System.Linq;
namespace Timeline.Models.Validation
{
public class UsernameValidator : Validator<string>
{
public const int MaxLength = 26;
- public const string RegexPattern = @"^[a-zA-Z0-9_][a-zA-Z0-9-_]*$";
- private readonly Regex _regex = new Regex(RegexPattern);
-
- protected override bool DoValidate(string value, out string message)
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base class.")]
+ protected override (bool, ValidationMessageGenerator) DoValidate(string value)
{
if (value.Length == 0)
{
- message = "An empty string is not permitted.";
- return false;
+ return (false, factory =>
+ factory?.Create(typeof(UsernameValidator))?["ValidationMessageEmptyString"]
+ ?? Resources.Models.Validation.UsernameValidator.InvariantValidationMessageEmptyString);
}
if (value.Length > 26)
{
- message = $"Too long, more than 26 characters is not premitted, found {value.Length}.";
- return false;
+ return (false, factory =>
+ factory?.Create(typeof(UsernameValidator))?["ValidationMessageTooLong"]
+ ?? Resources.Models.Validation.UsernameValidator.InvariantValidationMessageTooLong);
}
foreach ((char c, int i) in value.Select((c, i) => (c, i)))
- if (char.IsWhiteSpace(c))
+ {
+ if (!(char.IsLetterOrDigit(c) || c == '-' || c == '_'))
{
- message = $"A whitespace is found at {i} . Whitespace is not permited.";
- return false;
+ return (false, factory =>
+ factory?.Create(typeof(UsernameValidator))?["ValidationMessageInvalidChar"]
+ ?? Resources.Models.Validation.UsernameValidator.InvariantValidationMessageInvalidChar);
}
-
- var match = _regex.Match(value);
- if (!match.Success)
- {
- message = "Regex match failed.";
- return false;
}
- message = ValidationConstants.SuccessMessage;
- return true;
+ return (true, SuccessMessageGenerator);
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
+ AllowMultiple = false)]
+ public class UsernameAttribute : ValidateWithAttribute
+ {
+ public UsernameAttribute()
+ : base(typeof(UsernameValidator))
+ {
+
}
}
}
diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a1acbed9..d2c7c377 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -1,11 +1,21 @@ -using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
+using System;
using System.ComponentModel.DataAnnotations;
+using Timeline.Helpers;
namespace Timeline.Models.Validation
{
/// <summary>
+ /// Generate a message from a localizer factory.
+ /// If localizerFactory is null, it should return a culture-invariant message.
+ /// </summary>
+ /// <param name="localizerFactory">The localizer factory. Could be null.</param>
+ /// <returns>The message.</returns>
+ public delegate string ValidationMessageGenerator(IStringLocalizerFactory? localizerFactory);
+
+ /// <summary>
/// A validator to validate value.
- /// See <see cref="Validate(object, out string)"/>.
/// </summary>
public interface IValidator
{
@@ -13,14 +23,8 @@ namespace Timeline.Models.Validation /// Validate given value.
/// </summary>
/// <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);
- }
-
- public static class ValidationConstants
- {
- public const string SuccessMessage = "Validation succeeded.";
+ /// <returns>Validation success or not and the message generator.</returns>
+ (bool, ValidationMessageGenerator) Validate(object? value);
}
/// <summary>
@@ -36,27 +40,32 @@ namespace Timeline.Models.Validation /// </remarks>
public abstract class Validator<T> : IValidator
{
- public bool Validate(object value, out string message)
+ public (bool, ValidationMessageGenerator) Validate(object? value)
{
if (value == null)
{
- message = "Value is null.";
- return false;
+ return (false, factory =>
+ factory?.Create("Models.Validation.Validator")?["ValidatorMessageNull"]
+ ?? Resources.Models.Validation.Validator.InvariantValidatorMessageNull
+ );
}
if (value is T v)
{
-
- return DoValidate(v, out message);
+ return DoValidate(v);
}
else
{
- message = $"Value is not of type {typeof(T).Name}";
- return false;
+ return (false, factory =>
+ factory?.Create("Models.Validation.Validator")?["ValidatorMessageBadType", typeof(T).FullName]
+ ?? Resources.Models.Validation.Validator.InvariantValidatorMessageBadType);
}
}
- protected abstract bool DoValidate(T value, out string message);
+ protected static ValidationMessageGenerator SuccessMessageGenerator { get; } = factory =>
+ factory?.Create("Models.Validation.Validator")?["ValidatorMessageSuccess"] ?? Resources.Models.Validation.Validator.InvariantValidatorMessageSuccess;
+
+ protected abstract (bool, ValidationMessageGenerator) DoValidate(T value);
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
@@ -84,24 +93,33 @@ 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 (result, messageGenerator) = _validator.Validate(value);
+ if (result)
+ {
return ValidationResult.Success;
+ }
else
- return new ValidationResult(string.Format("Field {0} is bad. {1}", validationContext.DisplayName, message));
+ {
+ var localizerFactory = validationContext.GetRequiredService<IStringLocalizerFactory>();
+ return new ValidationResult(messageGenerator(localizerFactory));
+ }
}
}
}
diff --git a/Timeline/Program.cs b/Timeline/Program.cs index dfc93b9e..4a098adf 100644 --- a/Timeline/Program.cs +++ b/Timeline/Program.cs @@ -3,10 +3,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
+using System.Resources;
namespace Timeline
{
- public class Program
+ public static class Program
{
public static void Main(string[] args)
{
diff --git a/Timeline/Resources/Authentication/AuthHandler.Designer.cs b/Timeline/Resources/Authentication/AuthHandler.Designer.cs new file mode 100644 index 00000000..fd4540ea --- /dev/null +++ b/Timeline/Resources/Authentication/AuthHandler.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------
+// <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.Authentication {
+ 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 AuthHandler {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal AuthHandler() {
+ }
+
+ /// <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.Authentication.AuthHandler", typeof(AuthHandler).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 Token is found in authorization header. Token is {0} ..
+ /// </summary>
+ internal static string LogTokenFoundInHeader {
+ get {
+ return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} ..
+ /// </summary>
+ internal static string LogTokenFoundInQuery {
+ get {
+ return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to No jwt token is found..
+ /// </summary>
+ internal static string LogTokenNotFound {
+ get {
+ return ResourceManager.GetString("LogTokenNotFound", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A jwt token validation failed..
+ /// </summary>
+ internal static string LogTokenValidationFail {
+ get {
+ return ResourceManager.GetString("LogTokenValidationFail", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Authentication/AuthHandler.resx b/Timeline/Resources/Authentication/AuthHandler.resx new file mode 100644 index 00000000..4cddc8ce --- /dev/null +++ b/Timeline/Resources/Authentication/AuthHandler.resx @@ -0,0 +1,132 @@ +<?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="LogTokenFoundInHeader" xml:space="preserve">
+ <value>Token is found in authorization header. Token is {0} .</value>
+ </data>
+ <data name="LogTokenFoundInQuery" xml:space="preserve">
+ <value>Token is found in query param with key "{0}". Token is {1} .</value>
+ </data>
+ <data name="LogTokenNotFound" xml:space="preserve">
+ <value>No jwt token is found.</value>
+ </data>
+ <data name="LogTokenValidationFail" xml:space="preserve">
+ <value>A jwt token validation failed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Common.Designer.cs b/Timeline/Resources/Common.Designer.cs new file mode 100644 index 00000000..4f1c8e3f --- /dev/null +++ b/Timeline/Resources/Common.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------
+// <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 {
+ 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 Common {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Common() {
+ }
+
+ /// <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.Common", typeof(Common).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 branch is invalid. Normally this branch is not reachable..
+ /// </summary>
+ internal static string ExceptionInvalidBranch {
+ get {
+ return ResourceManager.GetString("ExceptionInvalidBranch", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Common.resx b/Timeline/Resources/Common.resx new file mode 100644 index 00000000..8a036996 --- /dev/null +++ b/Timeline/Resources/Common.resx @@ -0,0 +1,123 @@ +<?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="ExceptionInvalidBranch" xml:space="preserve">
+ <value>The branch is invalid. Normally this branch is not reachable.</value>
+ </data>
+</root>
\ No newline at end of file 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/TokenController.en.resx b/Timeline/Resources/Controllers/TokenController.en.resx new file mode 100644 index 00000000..4a3d94f9 --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.en.resx @@ -0,0 +1,135 @@ +<?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="ErrorBadCredential" xml:space="preserve">
+ <value>Username or password is invalid.</value>
+ </data>
+ <data name="ErrorVerifyBadFormat" xml:space="preserve">
+ <value>The token is of bad format. It might not be created by the server.</value>
+ </data>
+ <data name="ErrorVerifyExpire" xml:space="preserve">
+ <value>The token is expired.</value>
+ </data>
+ <data name="ErrorVerifyOldVersion" xml:space="preserve">
+ <value>Token has an old version. User might have update some info.</value>
+ </data>
+ <data name="ErrorVerifyUserNotExist" xml:space="preserve">
+ <value>User does not exist. Administrator might have deleted this user.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/TokenController.resx b/Timeline/Resources/Controllers/TokenController.resx new file mode 100644 index 00000000..683d6cc9 --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.resx @@ -0,0 +1,150 @@ +<?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="LogBadPassword" xml:space="preserve">
+ <value>The password is wrong.</value>
+ </data>
+ <data name="LogCreateFailure" xml:space="preserve">
+ <value>A user failed to create a token.</value>
+ </data>
+ <data name="LogCreateSuccess" xml:space="preserve">
+ <value>A user succeeded to create a token.</value>
+ </data>
+ <data name="LogUserNotExist" xml:space="preserve">
+ <value>The user does not exist.</value>
+ </data>
+ <data name="LogVerifyBadFormat" xml:space="preserve">
+ <value>The token is of bad format. It might not be created by the server.</value>
+ </data>
+ <data name="LogVerifyExpire" xml:space="preserve">
+ <value>The token is expired.</value>
+ </data>
+ <data name="LogVerifyFailure" xml:space="preserve">
+ <value>A token failed to be verified.</value>
+ </data>
+ <data name="LogVerifyOldVersion" xml:space="preserve">
+ <value>Token has an old version. User might have update some info.</value>
+ </data>
+ <data name="LogVerifySuccess" xml:space="preserve">
+ <value>A token succeeded to be verified.</value>
+ </data>
+ <data name="LogVerifyUserNotExist" xml:space="preserve">
+ <value>User does not exist. Administrator might have deleted this user.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/TokenController.zh.resx b/Timeline/Resources/Controllers/TokenController.zh.resx new file mode 100644 index 00000000..51e0f25b --- /dev/null +++ b/Timeline/Resources/Controllers/TokenController.zh.resx @@ -0,0 +1,135 @@ +<?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="ErrorBadCredential" xml:space="preserve">
+ <value>用户名或密码错误。</value>
+ </data>
+ <data name="ErrorVerifyBadFormat" xml:space="preserve">
+ <value>符号格式错误。这个符号可能不是这个服务器创建的。</value>
+ </data>
+ <data name="ErrorVerifyExpire" xml:space="preserve">
+ <value>符号过期了。</value>
+ </data>
+ <data name="ErrorVerifyOldVersion" xml:space="preserve">
+ <value>符号是一个旧版本。用户可能已经更新了信息。</value>
+ </data>
+ <data name="ErrorVerifyUserNotExist" xml:space="preserve">
+ <value>用户不存在。管理员可能已经删除了这个用户。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs new file mode 100644 index 00000000..e6eeb1e8 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.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 UserAvatarController {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarController() {
+ }
+
+ /// <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.UserAvatarController", typeof(UserAvatarController).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 Unknown AvatarDataException.ErrorReason value..
+ /// </summary>
+ internal static string ExceptionUnknownAvatarFormatError {
+ get {
+ return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogDeleteForbid {
+ get {
+ return ResourceManager.GetString("LogDeleteForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogDeleteNotExist {
+ get {
+ return ResourceManager.GetString("LogDeleteNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to delete a avatar of a user..
+ /// </summary>
+ internal static string LogDeleteSuccess {
+ get {
+ return ResourceManager.GetString("LogDeleteSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to get a avatar with If-None-Match in bad format..
+ /// </summary>
+ internal static string LogGetBadIfNoneMatch {
+ get {
+ return ResourceManager.GetString("LogGetBadIfNoneMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Returned full data for a get avatar attempt..
+ /// </summary>
+ internal static string LogGetReturnData {
+ get {
+ return ResourceManager.GetString("LogGetReturnData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Returned NotModify for a get avatar attempt..
+ /// </summary>
+ internal static string LogGetReturnNotModify {
+ get {
+ return ResourceManager.GetString("LogGetReturnNotModify", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogGetUserNotExist {
+ get {
+ return ResourceManager.GetString("LogGetUserNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed..
+ /// </summary>
+ internal static string LogPutForbid {
+ get {
+ return ResourceManager.GetString("LogPutForbid", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Succeed to put a avatar of a user..
+ /// </summary>
+ internal static string LogPutSuccess {
+ get {
+ return ResourceManager.GetString("LogPutSuccess", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed..
+ /// </summary>
+ internal static string LogPutUserBadFormat {
+ get {
+ return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed..
+ /// </summary>
+ internal static string LogPutUserNotExist {
+ get {
+ return ResourceManager.GetString("LogPutUserNotExist", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Controllers/UserAvatarController.en.resx b/Timeline/Resources/Controllers/UserAvatarController.en.resx new file mode 100644 index 00000000..cf92ae6d --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.en.resx @@ -0,0 +1,144 @@ +<?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="ErrorDeleteForbid" xml:space="preserve">
+ <value>Normal user can't delete other's avatar.</value>
+ </data>
+ <data name="ErrorDeleteUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+ <data name="ErrorPutBadFormatBadSize" xml:space="preserve">
+ <value>Image is not a square.</value>
+ </data>
+ <data name="ErrorPutBadFormatCantDecode" xml:space="preserve">
+ <value>Decoding image failed.</value>
+ </data>
+ <data name="ErrorPutBadFormatUnmatchedFormat" xml:space="preserve">
+ <value>Image format is not the one in header.</value>
+ </data>
+ <data name="ErrorPutForbid" xml:space="preserve">
+ <value>Normal user can't change other's avatar.</value>
+ </data>
+ <data name="ErrorPutUserNotExist" xml:space="preserve">
+ <value>User does not exist.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx new file mode 100644 index 00000000..58860c83 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.resx @@ -0,0 +1,156 @@ +<?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="ExceptionUnknownAvatarFormatError" xml:space="preserve">
+ <value>Unknown AvatarDataException.ErrorReason value.</value>
+ </data>
+ <data name="LogDeleteForbid" xml:space="preserve">
+ <value>Attempt to delete a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogDeleteNotExist" xml:space="preserve">
+ <value>Attempt to delete a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogDeleteSuccess" xml:space="preserve">
+ <value>Succeed to delete a avatar of a user.</value>
+ </data>
+ <data name="LogGetBadIfNoneMatch" xml:space="preserve">
+ <value>Attempt to get a avatar with If-None-Match in bad format.</value>
+ </data>
+ <data name="LogGetReturnData" xml:space="preserve">
+ <value>Returned full data for a get avatar attempt.</value>
+ </data>
+ <data name="LogGetReturnNotModify" xml:space="preserve">
+ <value>Returned NotModify for a get avatar attempt.</value>
+ </data>
+ <data name="LogGetUserNotExist" xml:space="preserve">
+ <value>Attempt to get a avatar of a non-existent user failed.</value>
+ </data>
+ <data name="LogPutForbid" xml:space="preserve">
+ <value>Attempt to put a avatar of other user as a non-admin failed.</value>
+ </data>
+ <data name="LogPutSuccess" xml:space="preserve">
+ <value>Succeed to put a avatar of a user.</value>
+ </data>
+ <data name="LogPutUserBadFormat" xml:space="preserve">
+ <value>Attempt to put a avatar of a bad format failed.</value>
+ </data>
+ <data name="LogPutUserNotExist" xml:space="preserve">
+ <value>Attempt to put a avatar of a non-existent user failed.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.zh.resx b/Timeline/Resources/Controllers/UserAvatarController.zh.resx new file mode 100644 index 00000000..94de1606 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.zh.resx @@ -0,0 +1,144 @@ +<?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="ErrorDeleteForbid" xml:space="preserve">
+ <value>普通用户不能删除其他用户的头像。</value>
+ </data>
+ <data name="ErrorDeleteUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+ <data name="ErrorPutBadFormatBadSize" xml:space="preserve">
+ <value>图片不是正方形。</value>
+ </data>
+ <data name="ErrorPutBadFormatCantDecode" xml:space="preserve">
+ <value>解码图片失败。</value>
+ </data>
+ <data name="ErrorPutBadFormatUnmatchedFormat" xml:space="preserve">
+ <value>图片格式与请求头中指示的不一样。</value>
+ </data>
+ <data name="ErrorPutForbid" xml:space="preserve">
+ <value>普通用户不能修改其他用户的头像。</value>
+ </data>
+ <data name="ErrorPutUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+</root>
\ No newline at end of file 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'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/Controllers/UserController.en.resx b/Timeline/Resources/Controllers/UserController.en.resx new file mode 100644 index 00000000..0bd1dfe3 --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.en.resx @@ -0,0 +1,138 @@ +<?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="ErrorChangePasswordBadPassword" xml:space="preserve">
+ <value>Old password is wrong.</value>
+ </data>
+ <data name="ErrorChangeUsernameAlreadyExist" xml:space="preserve">
+ <value>The new username {0} already exists.</value>
+ </data>
+ <data name="ErrorChangeUsernameNotExist" xml:space="preserve">
+ <value>The old username {0} does not exist.</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>The user does not exist.</value>
+ </data>
+ <data name="ErrorPatchUserNotExist" xml:space="preserve">
+ <value>Can't patch a user that does not exist.</value>
+ </data>
+ <data name="ErrorPutBadUsername" xml:space="preserve">
+ <value>Username is of bad format.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.resx b/Timeline/Resources/Controllers/UserController.resx new file mode 100644 index 00000000..d720d1c1 --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.resx @@ -0,0 +1,156 @@ +<?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="LogChangePasswordBadPassword" xml:space="preserve">
+ <value>Attempt to change password with wrong old password failed.</value>
+ </data>
+ <data name="LogChangePasswordSuccess" xml:space="preserve">
+ <value>A user has changed password.</value>
+ </data>
+ <data name="LogChangeUsernameAlreadyExist" xml:space="preserve">
+ <value>Attempt to change a user's username to a existent one failed.</value>
+ </data>
+ <data name="LogChangeUsernameNotExist" xml:space="preserve">
+ <value>Attempt to change a username of a user that does not exist failed.</value>
+ </data>
+ <data name="LogChangeUsernameSuccess" xml:space="preserve">
+ <value>A user has changed username.</value>
+ </data>
+ <data name="LogDeleteDelete" xml:space="preserve">
+ <value>A user has been deleted.</value>
+ </data>
+ <data name="LogDeleteNotExist" xml:space="preserve">
+ <value>Attempt to delete a user that does not exist.</value>
+ </data>
+ <data name="LogGetUserNotExist" xml:space="preserve">
+ <value>Attempt to retrieve info of a user that does not exist failed.</value>
+ </data>
+ <data name="LogPatchUserNotExist" xml:space="preserve">
+ <value>Attempt to patch a user that does not exist failed.</value>
+ </data>
+ <data name="LogPutBadUsername" xml:space="preserve">
+ <value>Attempt to create a user with bad username failed.</value>
+ </data>
+ <data name="LogPutCreate" xml:space="preserve">
+ <value>A user has been created.</value>
+ </data>
+ <data name="LogPutModify" xml:space="preserve">
+ <value>A user has been modified.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserController.zh.resx b/Timeline/Resources/Controllers/UserController.zh.resx new file mode 100644 index 00000000..3556083e --- /dev/null +++ b/Timeline/Resources/Controllers/UserController.zh.resx @@ -0,0 +1,138 @@ +<?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="ErrorChangePasswordBadPassword" xml:space="preserve">
+ <value>旧密码错误。</value>
+ </data>
+ <data name="ErrorChangeUsernameAlreadyExist" xml:space="preserve">
+ <value>新用户名{0}已经存在。</value>
+ </data>
+ <data name="ErrorChangeUsernameNotExist" xml:space="preserve">
+ <value>旧用户名{0}不存在。</value>
+ </data>
+ <data name="ErrorGetUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
+ <data name="ErrorPatchUserNotExist" xml:space="preserve">
+ <value>不能修改一个不存在的用户。</value>
+ </data>
+ <data name="ErrorPutBadUsername" xml:space="preserve">
+ <value>用户名格式错误。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx new file mode 100644 index 00000000..10407d76 --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.en.resx @@ -0,0 +1,153 @@ +<?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="ContentTooBig" xml:space="preserve">
+ <value>Body is too big. It can't be bigger than {0}.</value>
+ </data>
+ <data name="ContentUnmatchedLengthBigger" xml:space="preserve">
+ <value>Actual body length is bigger than it in header.</value>
+ </data>
+ <data name="ContentUnmatchedLengthSmaller" xml:space="preserve">
+ <value>Actual body length is smaller than it in header.</value>
+ </data>
+ <data name="DeleteDelete" xml:space="preserve">
+ <value>An existent item is deleted.</value>
+ </data>
+ <data name="DeleteNotExist" xml:space="preserve">
+ <value>The item does not exist, so nothing is changed.</value>
+ </data>
+ <data name="HeaderBadIfNonMatch" xml:space="preserve">
+ <value>Header If-Non-Match is of bad format.</value>
+ </data>
+ <data name="HeaderMissingContentLength" xml:space="preserve">
+ <value>Header Content-Length is missing or of bad format.</value>
+ </data>
+ <data name="HeaderMissingContentType" xml:space="preserve">
+ <value>Header Content-Type is required.</value>
+ </data>
+ <data name="HeaderZeroContentLength" xml:space="preserve">
+ <value>Header Content-Length must not be 0.</value>
+ </data>
+ <data name="PutCreate" xml:space="preserve">
+ <value>A new item is created.</value>
+ </data>
+ <data name="PutModify" xml:space="preserve">
+ <value>An existent item is modified.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx new file mode 100644 index 00000000..528dc7ab --- /dev/null +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -0,0 +1,153 @@ +<?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="ContentTooBig" xml:space="preserve">
+ <value>请求体太大。它不能超过{0}.</value>
+ </data>
+ <data name="ContentUnmatchedLengthBigger" xml:space="preserve">
+ <value>实际的请求体长度比头中指示的大。</value>
+ </data>
+ <data name="ContentUnmatchedLengthSmaller" xml:space="preserve">
+ <value>实际的请求体长度比头中指示的小。</value>
+ </data>
+ <data name="DeleteDelete" xml:space="preserve">
+ <value>删除了一个项目。</value>
+ </data>
+ <data name="DeleteNotExist" xml:space="preserve">
+ <value>要删除的项目不存在,什么都没有修改。</value>
+ </data>
+ <data name="HeaderBadIfNonMatch" xml:space="preserve">
+ <value>头If-Non-Match格式不对。</value>
+ </data>
+ <data name="HeaderMissingContentLength" xml:space="preserve">
+ <value>头Content-Length缺失或者格式不对。</value>
+ </data>
+ <data name="HeaderMissingContentType" xml:space="preserve">
+ <value>缺少必需的头Content-Type。</value>
+ </data>
+ <data name="HeaderZeroContentLength" xml:space="preserve">
+ <value>头Content-Length不能为0。</value>
+ </data>
+ <data name="PutCreate" xml:space="preserve">
+ <value>创建了一个新项目。</value>
+ </data>
+ <data name="PutModify" xml:space="preserve">
+ <value>修改了一个已存在的项目。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.Designer.cs b/Timeline/Resources/Models/Validation/UsernameValidator.Designer.cs new file mode 100644 index 00000000..a4c35326 --- /dev/null +++ b/Timeline/Resources/Models/Validation/UsernameValidator.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------
+// <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 UsernameValidator {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UsernameValidator() {
+ }
+
+ /// <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.UsernameValidator", typeof(UsernameValidator).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 An empty string is not allowed..
+ /// </summary>
+ internal static string InvariantValidationMessageEmptyString {
+ get {
+ return ResourceManager.GetString("InvariantValidationMessageEmptyString", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Invalid character, only alphabet, digit, underscore and hyphen are allowed. .
+ /// </summary>
+ internal static string InvariantValidationMessageInvalidChar {
+ get {
+ return ResourceManager.GetString("InvariantValidationMessageInvalidChar", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Too long, more than 26 characters is not premitted..
+ /// </summary>
+ internal static string InvariantValidationMessageTooLong {
+ get {
+ return ResourceManager.GetString("InvariantValidationMessageTooLong", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.en.resx b/Timeline/Resources/Models/Validation/UsernameValidator.en.resx new file mode 100644 index 00000000..9171b856 --- /dev/null +++ b/Timeline/Resources/Models/Validation/UsernameValidator.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="ValidationMessageEmptyString" xml:space="preserve">
+ <value>An empty string is not allowed.</value>
+ </data>
+ <data name="ValidationMessageInvalidChar" xml:space="preserve">
+ <value>Invalid character, only alphabet, digit, underscore and hyphen are allowed.</value>
+ </data>
+ <data name="ValidationMessageTooLong" xml:space="preserve">
+ <value>Too long, more than 26 characters is not premitted.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.resx b/Timeline/Resources/Models/Validation/UsernameValidator.resx new file mode 100644 index 00000000..80cae2d5 --- /dev/null +++ b/Timeline/Resources/Models/Validation/UsernameValidator.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="InvariantValidationMessageEmptyString" xml:space="preserve">
+ <value>An empty string is not allowed.</value>
+ </data>
+ <data name="InvariantValidationMessageInvalidChar" xml:space="preserve">
+ <value>Invalid character, only alphabet, digit, underscore and hyphen are allowed. </value>
+ </data>
+ <data name="InvariantValidationMessageTooLong" xml:space="preserve">
+ <value>Too long, more than 26 characters is not premitted.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx b/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx new file mode 100644 index 00000000..1c8a936c --- /dev/null +++ b/Timeline/Resources/Models/Validation/UsernameValidator.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="ValidationMessageEmptyString" xml:space="preserve">
+ <value>空字符串是不允许的。</value>
+ </data>
+ <data name="ValidationMessageInvalidChar" xml:space="preserve">
+ <value>无效的字符,只能使用字母、数字、下划线和连字符。</value>
+ </data>
+ <data name="ValidationMessageTooLong" xml:space="preserve">
+ <value>太长了,不能大于26个字符。</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.Designer.cs b/Timeline/Resources/Models/Validation/Validator.Designer.cs new file mode 100644 index 00000000..4cbc13de --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------
+// <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 Value is not of type {0}..
+ /// </summary>
+ internal static string InvariantValidatorMessageBadType {
+ get {
+ return ResourceManager.GetString("InvariantValidatorMessageBadType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Value can't be null..
+ /// </summary>
+ internal static string InvariantValidatorMessageNull {
+ get {
+ return ResourceManager.GetString("InvariantValidatorMessageNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Validation succeeded..
+ /// </summary>
+ internal static string InvariantValidatorMessageSuccess {
+ get {
+ return ResourceManager.GetString("InvariantValidatorMessageSuccess", resourceCulture);
+ }
+ }
+
+ /// <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..0e8f53a6 --- /dev/null +++ b/Timeline/Resources/Models/Validation/Validator.resx @@ -0,0 +1,135 @@ +<?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="InvariantValidatorMessageBadType" xml:space="preserve">
+ <value>Value is not of type {0}.</value>
+ </data>
+ <data name="InvariantValidatorMessageNull" xml:space="preserve">
+ <value>Value can't be null.</value>
+ </data>
+ <data name="InvariantValidatorMessageSuccess" xml:space="preserve">
+ <value>Validation succeeded.</value>
+ </data>
+ <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..ddf60f45 --- /dev/null +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------
+// <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 Avartar is of bad format because {0}..
+ /// </summary>
+ internal static string AvatarFormatException {
+ get {
+ return ResourceManager.GetString("AvatarFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image is not a square, aka, width is not equal to height.
+ /// </summary>
+ internal static string AvatarFormatExceptionBadSize {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionBadSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to failed to decode image, see inner exception.
+ /// </summary>
+ internal static string AvatarFormatExceptionCantDecode {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionCantDecode", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to unknown error.
+ /// </summary>
+ internal static string AvatarFormatExceptionUnknownError {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionUnknownError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to image's actual mime type is not the specified one.
+ /// </summary>
+ internal static string AvatarFormatExceptionUnmatchedFormat {
+ get {
+ return ResourceManager.GetString("AvatarFormatExceptionUnmatchedFormat", resourceCulture);
+ }
+ }
+
+ /// <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 hashes password is of bad format. It might not be created by server..
+ /// </summary>
+ internal static string HashedPasswordBadFromatException {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Not of valid base64 format. See inner exception..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotBase64 {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotBase64", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Decoded hashed password is of length 0..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotLength0 {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotLength0", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to See inner exception..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotOthers {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotOthers", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Salt length < 128 bits..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotSaltTooShort {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSaltTooShort", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Subkey length < 128 bits..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotSubkeyTooShort {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotSubkeyTooShort", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown format marker..
+ /// </summary>
+ internal static string HashedPasswordBadFromatExceptionNotUnknownMarker {
+ get {
+ return ResourceManager.GetString("HashedPasswordBadFromatExceptionNotUnknownMarker", 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'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..12bf9afb --- /dev/null +++ b/Timeline/Resources/Services/Exception.resx @@ -0,0 +1,198 @@ +<?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="AvatarFormatException" xml:space="preserve">
+ <value>Avartar is of bad format because {0}.</value>
+ </data>
+ <data name="AvatarFormatExceptionBadSize" xml:space="preserve">
+ <value>image is not a square, aka, width is not equal to height</value>
+ </data>
+ <data name="AvatarFormatExceptionCantDecode" xml:space="preserve">
+ <value>failed to decode image, see inner exception</value>
+ </data>
+ <data name="AvatarFormatExceptionUnknownError" xml:space="preserve">
+ <value>unknown error</value>
+ </data>
+ <data name="AvatarFormatExceptionUnmatchedFormat" xml:space="preserve">
+ <value>image's actual mime type is not the specified one</value>
+ </data>
+ <data name="BadPasswordException" xml:space="preserve">
+ <value>The password is wrong.</value>
+ </data>
+ <data name="HashedPasswordBadFromatException" xml:space="preserve">
+ <value>The hashes password is of bad format. It might not be created by server.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotBase64" xml:space="preserve">
+ <value>Not of valid base64 format. See inner exception.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotLength0" xml:space="preserve">
+ <value>Decoded hashed password is of length 0.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotOthers" xml:space="preserve">
+ <value>See inner exception.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotSaltTooShort" xml:space="preserve">
+ <value>Salt length < 128 bits.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotSubkeyTooShort" xml:space="preserve">
+ <value>Subkey length < 128 bits.</value>
+ </data>
+ <data name="HashedPasswordBadFromatExceptionNotUnknownMarker" xml:space="preserve">
+ <value>Unknown format marker.</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/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs new file mode 100644 index 00000000..6ee6fef9 --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------
+// <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 UserAvatarService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserAvatarService() {
+ }
+
+ /// <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.UserAvatarService", typeof(UserAvatarService).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 Data of avatar is null..
+ /// </summary>
+ internal static string ArgumentAvatarDataNull {
+ get {
+ return ResourceManager.GetString("ArgumentAvatarDataNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Type of avatar is null or empty..
+ /// </summary>
+ internal static string ArgumentAvatarTypeNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ArgumentAvatarTypeNullOrEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not..
+ /// </summary>
+ internal static string DatabaseCorruptedDataAndTypeNotSame {
+ get {
+ return ResourceManager.GetString("DatabaseCorruptedDataAndTypeNotSame", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Created an entry in user_avatars..
+ /// </summary>
+ internal static string LogCreateEntity {
+ get {
+ return ResourceManager.GetString("LogCreateEntity", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Updated an entry in user_avatars..
+ /// </summary>
+ internal static string LogUpdateEntity {
+ get {
+ return ResourceManager.GetString("LogUpdateEntity", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx new file mode 100644 index 00000000..3269bf13 --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.resx @@ -0,0 +1,135 @@ +<?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="ArgumentAvatarDataNull" xml:space="preserve">
+ <value>Data of avatar is null.</value>
+ </data>
+ <data name="ArgumentAvatarTypeNullOrEmpty" xml:space="preserve">
+ <value>Type of avatar is null or empty.</value>
+ </data>
+ <data name="DatabaseCorruptedDataAndTypeNotSame" xml:space="preserve">
+ <value>Database corupted! One of type and data of a avatar is null but the other is not.</value>
+ </data>
+ <data name="LogCreateEntity" xml:space="preserve">
+ <value>Created an entry in user_avatars.</value>
+ </data>
+ <data name="LogUpdateEntity" xml:space="preserve">
+ <value>Updated an entry in user_avatars.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs new file mode 100644 index 00000000..2a04dded --- /dev/null +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------
+// <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 UserService {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal UserService() {
+ }
+
+ /// <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.UserService", typeof(UserService).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 New username is of bad format..
+ /// </summary>
+ internal static string ExceptionNewUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionNewUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Old username is of bad format..
+ /// </summary>
+ internal static string ExceptionOldUsernameBadFormat {
+ get {
+ return ResourceManager.GetString("ExceptionOldUsernameBadFormat", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A cache entry is created..
+ /// </summary>
+ internal static string LogCacheCreate {
+ get {
+ return ResourceManager.GetString("LogCacheCreate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A cache entry is removed..
+ /// </summary>
+ internal static string LogCacheRemove {
+ get {
+ return ResourceManager.GetString("LogCacheRemove", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A new user entry is added to the database..
+ /// </summary>
+ internal static string LogDatabaseCreate {
+ get {
+ return ResourceManager.GetString("LogDatabaseCreate", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user entry is removed from the database..
+ /// </summary>
+ internal static string LogDatabaseRemove {
+ get {
+ return ResourceManager.GetString("LogDatabaseRemove", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to A user entry is updated to the database..
+ /// </summary>
+ internal static string LogDatabaseUpdate {
+ get {
+ return ResourceManager.GetString("LogDatabaseUpdate", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx new file mode 100644 index 00000000..3670d8f9 --- /dev/null +++ b/Timeline/Resources/Services/UserService.resx @@ -0,0 +1,141 @@ +<?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="ExceptionNewUsernameBadFormat" xml:space="preserve">
+ <value>New username is of bad format.</value>
+ </data>
+ <data name="ExceptionOldUsernameBadFormat" xml:space="preserve">
+ <value>Old username is of bad format.</value>
+ </data>
+ <data name="LogCacheCreate" xml:space="preserve">
+ <value>A cache entry is created.</value>
+ </data>
+ <data name="LogCacheRemove" xml:space="preserve">
+ <value>A cache entry is removed.</value>
+ </data>
+ <data name="LogDatabaseCreate" xml:space="preserve">
+ <value>A new user entry is added to the database.</value>
+ </data>
+ <data name="LogDatabaseRemove" xml:space="preserve">
+ <value>A user entry is removed from the database.</value>
+ </data>
+ <data name="LogDatabaseUpdate" xml:space="preserve">
+ <value>A user entry is updated to the database.</value>
+ </data>
+</root>
\ No newline at end of file diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs new file mode 100644 index 00000000..788eabb2 --- /dev/null +++ b/Timeline/Services/AvatarFormatException.cs @@ -0,0 +1,51 @@ +using System;
+using System.Globalization;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Thrown when avatar is of bad format.
+ /// </summary>
+ [Serializable]
+ public class AvatarFormatException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ BadSize
+ }
+
+ public AvatarFormatException() : base(MakeMessage(null)) { }
+ public AvatarFormatException(string message) : base(message) { }
+ public AvatarFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; }
+ public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; }
+
+ protected AvatarFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ private static string MakeMessage(ErrorReason? reason) =>
+ string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch
+ {
+ ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode,
+ ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat,
+ ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize,
+ _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError
+ });
+
+ public ErrorReason? Error { get; set; }
+ public Avatar? Avatar { get; set; }
+ }
+}
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/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index a37cf05b..62b22f00 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -4,22 +4,27 @@ using System.Collections.Generic; using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
- public static class DatabaseExtensions
+ internal static class DatabaseExtensions
{
/// <summary>
/// Check the existence and get the id of the user.
/// </summary>
/// <param name="username">The username of the user.</param>
/// <returns>The user id.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- public static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string username)
+ internal static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, UsernameValidator validator, string username)
{
- if (string.IsNullOrEmpty(username))
- throw new ArgumentException("Username is null or empty.", nameof(username));
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ var (result, messageGenerator) = validator.Validate(username);
+ if (!result)
+ throw new UsernameBadFormatException(username, messageGenerator(null));
var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync();
if (userId == 0)
diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs index e2abebdc..d328ea20 100644 --- a/Timeline/Services/ETagGenerator.cs +++ b/Timeline/Services/ETagGenerator.cs @@ -1,33 +1,45 @@ using System;
using System.Security.Cryptography;
+using System.Threading.Tasks;
namespace Timeline.Services
{
public interface IETagGenerator
{
- string Generate(byte[] source);
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
+ Task<string> Generate(byte[] source);
}
- public class ETagGenerator : IETagGenerator, IDisposable
+ public sealed class ETagGenerator : IETagGenerator, IDisposable
{
private readonly SHA1 _sha1;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")]
public ETagGenerator()
{
_sha1 = SHA1.Create();
}
- public string Generate(byte[] source)
+ public Task<string> Generate(byte[] source)
{
- if (source == null || source.Length == 0)
- throw new ArgumentException("Source is null or empty.", nameof(source));
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
- return Convert.ToBase64String(_sha1.ComputeHash(source));
+ return Task.Run(() => Convert.ToBase64String(_sha1.ComputeHash(source)));
}
+ private bool _disposed = false; // To detect redundant calls
+
public void Dispose()
{
+ if (_disposed) return;
_sha1.Dispose();
+ _disposed = true;
}
}
}
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 350c5e80..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,57 +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;
- }
-
- 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; private 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>
@@ -83,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);
}
@@ -110,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()
{
@@ -153,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
{
@@ -171,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/PasswordService.cs b/Timeline/Services/PasswordService.cs index e09a1365..e04a861b 100644 --- a/Timeline/Services/PasswordService.cs +++ b/Timeline/Services/PasswordService.cs @@ -12,13 +12,23 @@ namespace Timeline.Services [Serializable]
public class HashedPasswordBadFromatException : Exception
{
- public HashedPasswordBadFromatException(string hashedPassword, string message) : base(message) { HashedPassword = hashedPassword; }
- public HashedPasswordBadFromatException(string hashedPassword, string message, Exception inner) : base(message, inner) { HashedPassword = hashedPassword; }
+ private static string MakeMessage(string reason)
+ {
+ return Resources.Services.Exception.HashedPasswordBadFromatException + " Reason: " + reason;
+ }
+
+ public HashedPasswordBadFromatException() : base(Resources.Services.Exception.HashedPasswordBadFromatException) { }
+
+ public HashedPasswordBadFromatException(string message) : base(message) { }
+ public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { }
+
+ public HashedPasswordBadFromatException(string hashedPassword, string reason) : base(MakeMessage(reason)) { HashedPassword = hashedPassword; }
+ public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception inner) : base(MakeMessage(reason), inner) { HashedPassword = hashedPassword; }
protected HashedPasswordBadFromatException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
- public string HashedPassword { get; private set; }
+ public string? HashedPassword { get; set; }
}
public interface IPasswordService
@@ -140,22 +150,20 @@ namespace Timeline.Services }
catch (FormatException e)
{
- throw new HashedPasswordBadFromatException(hashedPassword, "Not of valid base64 format. See inner exception.", e);
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotBase64, e);
}
// read the format marker from the hashed password
if (decodedHashedPassword.Length == 0)
{
- throw new HashedPasswordBadFromatException(hashedPassword, "Decoded hashed password is of length 0.");
+ throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotLength0);
}
- switch (decodedHashedPassword[0])
- {
- case 0x01:
- return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword);
- default:
- throw new HashedPasswordBadFromatException(hashedPassword, "Unknown format marker.");
- }
+ return (decodedHashedPassword[0]) switch
+ {
+ 0x01 => VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword),
+ _ => throw new HashedPasswordBadFromatException(hashedPassword, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotUnknownMarker),
+ };
}
private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString)
@@ -170,7 +178,7 @@ namespace Timeline.Services // Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Salt length < 128 bits.");
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSaltTooShort);
}
byte[] salt = new byte[saltLength];
Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
@@ -179,7 +187,7 @@ namespace Timeline.Services int subkeyLength = hashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Subkey length < 128 bits.");
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotSubkeyTooShort);
}
byte[] expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
@@ -193,7 +201,7 @@ namespace Timeline.Services // This should never occur except in the case of a malformed payload, where
// we might go off the end of the array. Regardless, a malformed payload
// implies verification failed.
- throw new HashedPasswordBadFromatException(hashedPasswordString, "See inner exception.", e);
+ throw new HashedPasswordBadFromatException(hashedPasswordString, Resources.Services.Exception.HashedPasswordBadFromatExceptionNotOthers, e);
}
}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ecec5a31..ff80003c 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -10,54 +10,25 @@ using System.IO; using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
namespace Timeline.Services
{
public class Avatar
{
- public string Type { get; set; }
- public byte[] Data { get; set; }
+ public string Type { get; set; } = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")]
+ public byte[] Data { get; set; } = default!;
}
public class AvatarInfo
{
- public Avatar Avatar { get; set; }
+ public Avatar Avatar { get; set; } = default!;
public DateTime LastModified { get; set; }
}
/// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarDataException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- BadSize
- }
-
- public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; }
- public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; }
- protected AvatarDataException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public ErrorReason Error { get; set; }
- public Avatar Avatar { get; set; }
- }
-
- /// <summary>
/// Provider for default user avatar.
/// </summary>
/// <remarks>
@@ -83,7 +54,7 @@ namespace Timeline.Services /// Validate a avatar's format and size info.
/// </summary>
/// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarDataException">Thrown when validation failed.</exception>
+ /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
Task Validate(Avatar avatar);
}
@@ -94,16 +65,18 @@ namespace Timeline.Services /// </summary>
/// <param name="username">The username of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<string> GetAvatarETag(string username);
/// <summary>
- /// Get avatar of a user. If the user has no avatar, a default one is returned.
+ /// Get avatar of a user. If the user has no avatar set, a default one is returned.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<AvatarInfo> GetAvatar(string username);
@@ -112,38 +85,41 @@ namespace Timeline.Services /// </summary>
/// <param name="username">The username of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentException">Throw if <paramref name="username"/> is null or empty.
- /// Or thrown if <paramref name="avatar"/> is not null but <see cref="Avatar.Type"/> is null or empty or <see cref="Avatar.Data"/> is null.</exception>
+ /// <exception cref="ArgumentNullException">Throw if <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- /// <exception cref="AvatarDataException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar avatar);
+ /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ Task SetAvatar(string username, Avatar? avatar);
}
+ // TODO! : Make this configurable.
public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
{
- private readonly IWebHostEnvironment _environment;
-
private readonly IETagGenerator _eTagGenerator;
- private byte[] _cacheData;
+ private readonly string _avatarPath;
+
+ private byte[] _cacheData = default!;
private DateTime _cacheLastModified;
- private string _cacheETag;
+ private string _cacheETag = default!;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")]
public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
{
- _environment = environment;
+ _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
_eTagGenerator = eTagGenerator;
}
private async Task CheckAndInit()
{
- if (_cacheData != null)
- return;
-
- var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png");
- _cacheData = await File.ReadAllBytesAsync(path);
- _cacheLastModified = File.GetLastWriteTime(path);
- _cacheETag = _eTagGenerator.Generate(_cacheData);
+ var path = _avatarPath;
+ if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified)
+ {
+ _cacheData = await File.ReadAllBytesAsync(path);
+ _cacheLastModified = File.GetLastWriteTime(path);
+ _cacheETag = await _eTagGenerator.Generate(_cacheData);
+ }
}
public async Task<string> GetDefaultAvatarETag()
@@ -175,17 +151,15 @@ namespace Timeline.Services {
try
{
- using (var image = Image.Load(avatar.Data, out IImageFormat format))
- {
- if (!format.MimeTypes.Contains(avatar.Type))
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one.");
- if (image.Width != image.Height)
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height.");
- }
+ using var image = Image.Load(avatar.Data, out IImageFormat format);
+ if (!format.MimeTypes.Contains(avatar.Type))
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat);
+ if (image.Width != image.Height)
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize);
}
catch (UnknownImageFormatException e)
{
- throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e);
+ throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e);
}
});
}
@@ -203,25 +177,32 @@ namespace Timeline.Services private readonly IETagGenerator _eTagGenerator;
+ private readonly UsernameValidator _usernameValidator;
+
+ private readonly IClock _clock;
+
public UserAvatarService(
ILogger<UserAvatarService> logger,
DatabaseContext database,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
IUserAvatarValidator avatarValidator,
- IETagGenerator eTagGenerator)
+ IETagGenerator eTagGenerator,
+ IClock clock)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
_eTagGenerator = eTagGenerator;
+ _usernameValidator = new UsernameValidator();
+ _clock = clock;
}
public async Task<string> GetAvatarETag(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag;
+ var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag;
if (eTag == null)
return await _defaultUserAvatarProvider.GetDefaultAvatarETag();
else
@@ -230,73 +211,88 @@ namespace Timeline.Services public async Task<AvatarInfo> GetAvatar(string username)
{
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
- var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync();
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync();
- if ((avatar.Type == null) != (avatar.Data == null))
+ if (avatarEntity != null)
{
- _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not.");
- throw new DatabaseCorruptedException();
- }
+ if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null))
+ {
+ var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame;
+ _logger.LogCritical(message);
+ throw new DatabaseCorruptedException(message);
+ }
- if (avatar.Data == null)
- {
- var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
- defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified;
- return defaultAvatar;
- }
- else
- {
- return new AvatarInfo
+ if (avatarEntity.Data != null)
{
- Avatar = new Avatar
+ return new AvatarInfo
{
- Type = avatar.Type,
- Data = avatar.Data
- },
- LastModified = avatar.LastModified
- };
+ Avatar = new Avatar
+ {
+ Type = avatarEntity.Type!,
+ Data = avatarEntity.Data
+ },
+ LastModified = avatarEntity.LastModified
+ };
+ }
}
+ var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar();
+ if (avatarEntity != null)
+ defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified;
+ return defaultAvatar;
}
- public async Task SetAvatar(string username, Avatar avatar)
+ public async Task SetAvatar(string username, Avatar? avatar)
{
if (avatar != null)
{
- if (string.IsNullOrEmpty(avatar.Type))
- throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar));
if (avatar.Data == null)
- throw new ArgumentException("Data of avatar is null.", nameof(avatar));
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar));
+ if (string.IsNullOrEmpty(avatar.Type))
+ throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNullOrEmpty, nameof(avatar));
}
- var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username);
-
- var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync();
+ var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username);
+ var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
if (avatar == null)
{
- if (avatarEntity.Data == null)
+ if (avatarEntity == null || avatarEntity.Data == null)
+ {
return;
+ }
else
{
avatarEntity.Data = null;
avatarEntity.Type = null;
avatarEntity.ETag = null;
- avatarEntity.LastModified = DateTime.Now;
+ avatarEntity.LastModified = _clock.GetCurrentTime();
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
else
{
await _avatarValidator.Validate(avatar);
- avatarEntity.Type = avatar.Type;
+ var create = avatarEntity == null;
+ if (create)
+ {
+ avatarEntity = new UserAvatar();
+ }
+ avatarEntity!.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
- avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data);
- avatarEntity.LastModified = DateTime.Now;
+ avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data);
+ avatarEntity.LastModified = _clock.GetCurrentTime();
+ avatarEntity.UserId = userId;
+ if (create)
+ {
+ _database.UserAvatars.Add(avatarEntity);
+ }
await _database.SaveChangesAsync();
- _logger.LogInformation("Updated an entry in user_avatars.");
+ _logger.LogInformation(create ?
+ Resources.Services.UserAvatarService.LogCreateEntity
+ : Resources.Services.UserAvatarService.LogUpdateEntity);
}
}
}
@@ -308,7 +304,7 @@ namespace Timeline.Services services.TryAddTransient<IETagGenerator, ETagGenerator>();
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddSingleton<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddSingleton<IUserAvatarValidator, UserAvatarValidator>();
+ services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
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 347b8cbb..8f354fc7 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -5,140 +5,16 @@ using System; using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
+using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
-using static Timeline.Helpers.MyLogHelper;
-using static Timeline.Models.UserUtility;
namespace Timeline.Services
{
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(string username)
- : base(FormatLogMessage(message, Pair("Username", username)))
- {
- Username = username;
- }
-
- public UserNotExistException(long id)
- : base(FormatLogMessage(message, Pair("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. May be null then <see cref="Id"/> is not null.
- /// </summary>
- public string Username { get; private set; }
-
- /// <summary>
- /// The id that does not exist. May be null then <see cref="Username"/> is not null.
- /// </summary>
- public long? Id { get; private set; }
- }
-
- [Serializable]
- public class BadPasswordException : Exception
- {
- public BadPasswordException(string badPassword)
- : base(FormatLogMessage("Password is wrong.", Pair("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; private set; }
- }
-
-
- [Serializable]
- public class BadTokenVersionException : Exception
- {
- public BadTokenVersionException(long tokenVersion, long requiredVersion)
- : base(FormatLogMessage("Token version is expired.",
- Pair("Token Version", tokenVersion),
- Pair("Required Version", requiredVersion)))
- {
- TokenVersion = tokenVersion;
- RequiredVersion = requiredVersion;
- }
-
- 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; private set; }
-
- /// <summary>
- /// The version required.
- /// </summary>
- public long RequiredVersion { get; private set; }
- }
-
- /// <summary>
- /// Thrown when username is of bad format.
- /// </summary>
- [Serializable]
- public class UsernameBadFormatException : Exception
- {
- 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
- {
- public UserAlreadyExistException(string username) : base($"User {username} already exists.") { 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
@@ -152,6 +28,7 @@ namespace Timeline.Services /// <param name="expires">The expired time point. Null then use default. See <see cref="JwtService.GenerateJwtToken(TokenInfo, DateTime?)"/> for what is default.</param>
/// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when username is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires = null);
@@ -163,9 +40,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>
@@ -173,6 +49,8 @@ namespace Timeline.Services /// </summary>
/// <param name="username">Username of the user.</param>
/// <returns>The info of the user. Null if the user of given username does not exists.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
Task<UserInfo> GetUser(string username);
/// <summary>
@@ -188,10 +66,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>
@@ -203,14 +83,16 @@ namespace Timeline.Services /// <param name="password">New password. Null if not modify.</param>
/// <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="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</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.
/// </summary>
/// <param name="username">Username of thet user to delete. Can't be null.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
Task DeleteUser(string username);
@@ -221,6 +103,7 @@ namespace Timeline.Services /// <param name="oldPassword">The user's old password.</param>
/// <param name="newPassword">The user's new password.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> or <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
Task ChangePassword(string username, string oldPassword, string newPassword);
@@ -230,16 +113,16 @@ namespace Timeline.Services /// </summary>
/// <param name="oldUsername">The user's old username.</param>
/// <param name="newUsername">The new username.</param>
- /// <exception cref="ArgumentException">Thrown if <paramref name="oldUsername"/> or <paramref name="newUsername"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldUsername"/> or <paramref name="newUsername"/> is null.</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="UsernameBadFormatException">Thrown if the <paramref name="oldUsername"/> or <paramref name="newUsername"/> is of bad format.</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; }
@@ -272,13 +155,25 @@ namespace Timeline.Services _usernameValidator = new UsernameValidator();
}
- private string GenerateCacheKeyByUserId(long id) => $"user:{id}";
+ private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
private void RemoveCache(long id)
{
var key = GenerateCacheKeyByUserId(id);
_memoryCache.Remove(key);
- _logger.LogInformation(FormatLogMessage("A cache entry is removed.", Pair("Key", key)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key)));
+ }
+
+ private void CheckUsernameFormat(string username, string? message = null)
+ {
+ var (result, messageGenerator) = _usernameValidator.Validate(username);
+ if (!result)
+ {
+ if (message == null)
+ throw new UsernameBadFormatException(username, messageGenerator(null));
+ else
+ throw new UsernameBadFormatException(username, message + messageGenerator(null));
+ }
}
public async Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires)
@@ -287,6 +182,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
+ CheckUsernameFormat(username);
// We need password info, so always check the database.
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
@@ -306,7 +202,7 @@ namespace Timeline.Services return new CreateTokenResult
{
Token = token,
- User = CreateUserInfo(user)
+ User = UserConvert.CreateUserInfo(user)
};
}
@@ -329,29 +225,33 @@ namespace Timeline.Services throw new UserNotExistException(id);
// create cache
- cache = CreateUserCache(user);
+ cache = UserConvert.CreateUserCache(user);
_memoryCache.CreateEntry(key).SetValue(cache);
- _logger.LogInformation(FormatLogMessage("A cache entry is created.", Pair("Key", key)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key)));
}
if (tokenInfo.Version != cache.Version)
- throw new BadTokenVersionException(tokenInfo.Version, cache.Version);
+ throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion);
return cache.ToUserInfo();
}
public async Task<UserInfo> GetUser(string username)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
+
return await _databaseContext.Users
.Where(user => user.Name == username)
- .Select(user => CreateUserInfo(user))
+ .Select(user => UserConvert.CreateUserInfo(user))
.SingleOrDefaultAsync();
}
public async Task<UserInfo[]> ListUsers()
{
return await _databaseContext.Users
- .Select(user => CreateUserInfo(user))
+ .Select(user => UserConvert.CreateUserInfo(user))
.ToArrayAsync();
}
@@ -361,11 +261,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
-
- if (!_usernameValidator.Validate(username, out var message))
- {
- throw new UsernameBadFormatException(username, message);
- }
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
@@ -375,31 +271,34 @@ namespace Timeline.Services {
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = IsAdminToRoleString(administrator),
- Avatar = UserAvatar.Create(DateTime.Now)
+ RoleString = UserRoleConvert.ToString(administrator),
+ Avatar = null
};
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;
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate,
+ ("Id", newUser.Id), ("Username", username), ("Administrator", administrator)));
+ return PutResult.Create;
}
user.EncryptedPassword = _passwordService.HashPassword(password);
- user.RoleString = IsAdminToRoleString(administrator);
+ user.RoleString = UserRoleConvert.ToString(administrator);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Username", username), ("Administrator", administrator)));
//clear cache
RemoveCache(user.Id);
- return PutResult.Modified;
+ return PutResult.Modify;
}
- public async Task PatchUser(string username, string password, bool? administrator)
+ public async Task PatchUser(string username, string? password, bool? administrator)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -412,12 +311,12 @@ namespace Timeline.Services if (administrator != null)
{
- user.RoleString = IsAdminToRoleString(administrator.Value);
+ user.RoleString = UserRoleConvert.ToString(administrator.Value);
}
user.Version += 1;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id));
//clear cache
RemoveCache(user.Id);
@@ -427,6 +326,7 @@ namespace Timeline.Services {
if (username == null)
throw new ArgumentNullException(nameof(username));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -434,7 +334,8 @@ namespace Timeline.Services _databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry is removed from the database.", Pair("Id", user.Id)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove,
+ ("Id", user.Id)));
//clear cache
RemoveCache(user.Id);
@@ -448,6 +349,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(oldPassword));
if (newPassword == null)
throw new ArgumentNullException(nameof(newPassword));
+ CheckUsernameFormat(username);
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -460,19 +362,20 @@ namespace Timeline.Services user.EncryptedPassword = _passwordService.HashPassword(newPassword);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Operation", "Change password")));
//clear cache
RemoveCache(user.Id);
}
public async Task ChangeUsername(string oldUsername, string newUsername)
{
- if (string.IsNullOrEmpty(oldUsername))
- throw new ArgumentException("Old username is null or empty", nameof(oldUsername));
- if (string.IsNullOrEmpty(newUsername))
- throw new ArgumentException("New username is null or empty", nameof(newUsername));
-
- if (!_usernameValidator.Validate(newUsername, out var message))
- throw new UsernameBadFormatException(newUsername, $"New username is of bad format. {message}");
+ if (oldUsername == null)
+ throw new ArgumentNullException(nameof(oldUsername));
+ if (newUsername == null)
+ throw new ArgumentNullException(nameof(newUsername));
+ CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat);
+ CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat);
var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync();
if (user == null)
@@ -480,13 +383,13 @@ 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;
await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(FormatLogMessage("A user entry changed name field.",
- Pair("Id", user.Id), Pair("Old Username", oldUsername), Pair("New Username", newUsername)));
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate,
+ ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername)));
RemoveCache(user.Id);
}
}
diff --git a/Timeline/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 8e8a6393..b44add6f 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -2,10 +2,13 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Timeline.Authenticate;
+using System.Collections.Generic;
+using System.Globalization;
+using Timeline.Authentication;
using Timeline.Configs;
using Timeline.Entities;
using Timeline.Helpers;
@@ -13,6 +16,7 @@ using Timeline.Services; namespace Timeline
{
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
@@ -27,11 +31,12 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddMvc()
+ services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory;
- });
+ })
+ .AddNewtonsoftJson();
services.Configure<JwtConfig>(Configuration.GetSection(nameof(JwtConfig)));
var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
@@ -48,13 +53,17 @@ namespace Timeline );
});
+ services.AddLocalization(options =>
+ {
+ options.ResourcesPath = "Resources";
+ });
+
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtService, JwtService>();
services.AddTransient<IPasswordService, PasswordService>();
services.AddTransient<IClock, Clock>();
services.AddUserAvatarService();
- services.AddUserDetailService();
var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>();
@@ -63,11 +72,10 @@ namespace Timeline options.UseMySql(databaseConfig.ConnectionString);
});
- services.AddHttpClient();
-
services.AddMemoryCache();
}
+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
@@ -78,6 +86,19 @@ namespace Timeline app.UseRouting();
+ var supportedCultures = new List<CultureInfo>
+ {
+ new CultureInfo("en"),
+ new CultureInfo("zh")
+ };
+
+ app.UseRequestLocalization(new RequestLocalizationOptions
+ {
+ DefaultRequestCulture = new RequestCulture("en"),
+ SupportedCultures = supportedCultures,
+ SupportedUICultures = supportedCultures
+ });
+
app.UseCors();
app.UseAuthentication();
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index f01b8e31..519a802d 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -4,6 +4,9 @@ <IsPackable>false</IsPackable>
<UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId>
<Authors>crupest</Authors>
+
+ <LangVersion>8.0</LangVersion>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -13,9 +16,127 @@ </ItemGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.6">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="3.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.0.0-rc1.final" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Design" Version="1.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-dev002868" />
- <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.5.0" />
+ <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="Resources\Authentication\AuthHandler.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>AuthHandler.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Common.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <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\UserAvatarController.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarController.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\UsernameValidator.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UsernameValidator.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>
+ <Compile Update="Resources\Services\UserAvatarService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserAvatarService.resx</DependentUpon>
+ </Compile>
+ <Compile Update="Resources\Services\UserService.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>UserService.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Update="Resources\Authentication\AuthHandler.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>AuthHandler.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Common.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>Common.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TokenController.resx">
+ <SubType>Designer</SubType>
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>TokenController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TokenController.zh.resx">
+ <SubType>Designer</SubType>
+ <Generator></Generator>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\TokenController.en.resx">
+ <Generator></Generator>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserAvatarController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Controllers\UserController.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserController.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\UsernameValidator.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UsernameValidator.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Models\Validation\Validator.en.resx">
+ <Generator></Generator>
+ </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>
+ <EmbeddedResource Update="Resources\Services\UserAvatarService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserAvatarService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ <EmbeddedResource Update="Resources\Services\UserService.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>UserService.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
</ItemGroup>
</Project>
|