From 4aadb05cd5718c7d16bf432c96e23ae4e7db4783 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 21 Jan 2020 01:11:17 +0800 Subject: ... --- Timeline/Services/UserTokenManager.cs | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 Timeline/Services/UserTokenManager.cs (limited to 'Timeline/Services/UserTokenManager.cs') diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs new file mode 100644 index 00000000..c3cb51c9 --- /dev/null +++ b/Timeline/Services/UserTokenManager.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Timeline.Models; + +namespace Timeline.Services +{ + public class UserTokenCreateResult + { + public string Token { get; set; } = default!; + public UserInfo User { get; set; } = default!; + } + + public interface IUserTokenManager + { + /// + /// Try to create a token for given username and password. + /// + /// The username. + /// The password. + /// The expire time of the token. + /// The created token and the user info. + /// Thrown when or is null. + /// Thrown when is of bad format. + /// Thrown when the user with does not exist. + /// Thrown when is wrong. + public Task CreateToken(string username, string password, DateTime? expireAt = null); + + /// + /// Verify a token and get the saved user info. This also check the database for existence of the user. + /// + /// The token. + /// The user stored in token. + /// Thrown when is null. + /// Thrown when the token is expired. + /// Thrown when the token is of bad version. + /// Thrown when the token is of bad format. + /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. + public Task VerifyToken(string token); + } + + public class UserTokenManager : IUserTokenManager + { + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IUserTokenService _userTokenService; + private readonly IClock _clock; + + public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) + { + _logger = logger; + _userService = userService; + _userTokenService = userTokenService; + _clock = clock; + } + + public async Task CreateToken(string username, string password, DateTime? expireAt = null) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + var user = await _userService.VerifyCredential(username, password); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); + + return new UserTokenCreateResult { Token = token, User = user }; + } + + + public async Task VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var tokenInfo = _userTokenService.VerifyToken(token); + + if (tokenInfo.ExpireAt.HasValue) + { + var currentTime = _clock.GetCurrentTime(); + if (tokenInfo.ExpireAt < currentTime) + throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime); + } + + var user = await _userService.GetUserById(tokenInfo.Id); + + if (tokenInfo.Version < user.Version) + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version); + + return user; + } + } +} -- cgit v1.2.3 From b6043126fae039c58512f60a576b10925b06df4c Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 00:17:45 +0800 Subject: ... --- Timeline.Tests/Controllers/TokenControllerTest.cs | 4 +- Timeline.Tests/Controllers/UserControllerTest.cs | 8 +- Timeline.Tests/Helpers/TestDatabase.cs | 6 +- Timeline.Tests/Services/UserAvatarServiceTest.cs | 6 +- Timeline.Tests/Services/UserDetailServiceTest.cs | 4 +- Timeline.Tests/Services/UserTokenManagerTest.cs | 8 +- Timeline/Controllers/TokenController.cs | 4 +- Timeline/Controllers/UserDetailController.cs | 49 ---- Timeline/Entities/DatabaseContext.cs | 2 +- Timeline/Entities/UserDetailEntity.cs | 21 -- Timeline/Entities/UserEntity.cs | 13 +- Timeline/GlobalSuppressions.cs | 1 + Timeline/Models/Http/User.cs | 9 +- Timeline/Models/User.cs | 19 ++ Timeline/Models/UserInfo.cs | 10 - Timeline/Models/Validation/Validator.cs | 28 ++- .../Validation/PasswordValidator.Designer.cs | 72 ++++++ .../Models/Validation/PasswordValidator.resx | 123 ++++++++++ .../Models/Validation/PasswordValidator.zh.resx | 123 ++++++++++ Timeline/Resources/Services/Exception.Designer.cs | 18 +- Timeline/Resources/Services/Exception.resx | 6 +- Timeline/Resources/Services/UserCache.Designer.cs | 99 ++++++++ Timeline/Resources/Services/UserCache.resx | 132 ++++++++++ .../Resources/Services/UserManager.Designer.cs | 72 ++++++ Timeline/Resources/Services/UserManager.resx | 123 ++++++++++ .../Resources/Services/UserService.Designer.cs | 36 +++ Timeline/Resources/Services/UserService.resx | 12 + Timeline/Services/DatabaseExtensions.cs | 2 +- Timeline/Services/PasswordBadFormatException.cs | 27 ++ Timeline/Services/TimelineService.cs | 16 +- Timeline/Services/UserDetailService.cs | 102 -------- Timeline/Services/UserNotExistException.cs | 4 +- Timeline/Services/UserService.cs | 271 +++++++++++---------- Timeline/Services/UserTokenManager.cs | 6 +- Timeline/Services/UsernameBadFormatException.cs | 30 --- Timeline/Timeline.csproj | 27 ++ 36 files changed, 1086 insertions(+), 407 deletions(-) delete mode 100644 Timeline/Controllers/UserDetailController.cs delete mode 100644 Timeline/Entities/UserDetailEntity.cs create mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/UserInfo.cs create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.resx create mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.zh.resx create mode 100644 Timeline/Resources/Services/UserCache.Designer.cs create mode 100644 Timeline/Resources/Services/UserCache.resx create mode 100644 Timeline/Resources/Services/UserManager.Designer.cs create mode 100644 Timeline/Resources/Services/UserManager.resx create mode 100644 Timeline/Services/PasswordBadFormatException.cs delete mode 100644 Timeline/Services/UserDetailService.cs delete mode 100644 Timeline/Services/UsernameBadFormatException.cs (limited to 'Timeline/Services/UserTokenManager.cs') diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 61fbe950..43e1a413 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -42,7 +42,7 @@ namespace Timeline.Tests.Controllers var mockCreateResult = new UserTokenCreateResult { Token = "mocktokenaaaaa", - User = new UserInfo + User = new Models.User { Id = 1, Username = MockUser.User.Username, @@ -99,7 +99,7 @@ namespace Timeline.Tests.Controllers public async Task Verify_Ok() { const string token = "aaaaaaaaaaaaaa"; - _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.VerifyToken(token)).ReturnsAsync(new Models.User { Id = 1, Username = MockUser.User.Username, diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index a1035675..192d53dd 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -36,9 +36,9 @@ namespace Timeline.Tests.Controllers [Fact] public async Task GetList_Success() { - var mockUserList = new UserInfo[] { - new UserInfo { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, - new UserInfo { Id = 2, Username = "bbb", Administrator = false, Version = 1 } + var mockUserList = new Models.User[] { + new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, + new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 } }; _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList); var action = await _controller.List(); @@ -51,7 +51,7 @@ namespace Timeline.Tests.Controllers public async Task Get_Success() { const string username = "aaa"; - _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(new Models.User { Id = 1, Username = MockUser.User.Username, diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs index 3163279a..e29a71fa 100644 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -18,9 +18,9 @@ namespace Timeline.Tests.Helpers { return new UserEntity { - Name = user.Username, - EncryptedPassword = PasswordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), + Username = user.Username, + Password = PasswordService.HashPassword(user.Password), + Roles = UserRoleConvert.ToString(user.Administrator), Avatar = null }; } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index d4371c48..2dca7ccf 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -171,7 +171,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { var context = _database.Context; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } @@ -205,7 +205,7 @@ namespace Timeline.Tests.Services var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { var context = _database.Context; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); } @@ -236,7 +236,7 @@ namespace Timeline.Tests.Services { string username = MockUser.User.Username; - var user = await _database.Context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await _database.Context.Users.Where(u => u.Username == username).Include(u => u.Avatar).SingleAsync(); var avatar1 = CreateMockAvatar("aaa"); var avatar2 = CreateMockAvatar("bbb"); diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index e6eabadf..dbff2705 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -51,7 +51,7 @@ namespace Timeline.Tests.Services const string nickname = "aaaaaa"; { var context = _testDatabase.Context; - var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; + var userId = (await context.Users.Where(u => u.Username == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; context.UserDetails.Add(new UserDetailEntity { Nickname = nickname, @@ -83,7 +83,7 @@ namespace Timeline.Tests.Services public async Task SetNickname_ShouldWork() { var username = MockUser.User.Username; - var user = await _testDatabase.Context.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); + var user = await _testDatabase.Context.Users.Where(u => u.Username == username).Include(u => u.Detail).SingleAsync(); var nickname1 = "nickname1"; var nickname2 = "nickname2"; diff --git a/Timeline.Tests/Services/UserTokenManagerTest.cs b/Timeline.Tests/Services/UserTokenManagerTest.cs index 19122d31..e649fbab 100644 --- a/Timeline.Tests/Services/UserTokenManagerTest.cs +++ b/Timeline.Tests/Services/UserTokenManagerTest.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Timeline.Models; using Timeline.Services; @@ -58,7 +56,7 @@ namespace Timeline.Tests.Services const string username = "uuu"; const string password = "ppp"; var mockExpireTime = setExpireTime ? (DateTime?)DateTime.Now : null; - var mockUserInfo = new UserInfo + var mockUserInfo = new User { Id = 1, Username = username, @@ -126,7 +124,7 @@ namespace Timeline.Tests.Services ExpireAt = mockTime.AddDays(1) }; _mockUserTokenService.Setup(s => s.VerifyToken(mockToken)).Returns(mockTokenInfo); - _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new UserInfo + _mockUserService.Setup(s => s.GetUserById(1)).ReturnsAsync(new User { Id = 1, Username = "aaa", @@ -149,7 +147,7 @@ namespace Timeline.Tests.Services Version = 1, ExpireAt = mockTime.AddDays(1) }; - var mockUserInfo = new UserInfo + var mockUserInfo = new User { Id = 1, Username = "aaa", diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index a96b6fa9..9724c1a6 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -20,9 +20,9 @@ namespace Timeline.Controllers private readonly ILogger _logger; private readonly IClock _clock; - private static User CreateUserFromUserInfo(UserInfo userInfo) + private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo) { - return new User + return new Models.Http.User { Username = userInfo.Username, Administrator = userInfo.Administrator diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs deleted file mode 100644 index 9de9899e..00000000 --- a/Timeline/Controllers/UserDetailController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using Timeline.Filters; -using Timeline.Models.Validation; -using Timeline.Services; -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Authorization; - -namespace Timeline.Controllers -{ - [ApiController] - public class UserDetailController : Controller - { - private readonly IUserDetailService _service; - - public UserDetailController(IUserDetailService service) - { - _service = service; - } - - [HttpGet("users/{username}/nickname")] - [CatchUserNotExistException] - public async Task> GetNickname([FromRoute][Username] string username) - { - return Ok(await _service.GetNickname(username)); - } - - [HttpPut("users/{username}/nickname")] - [Authorize] - [SelfOrAdmin] - [CatchUserNotExistException] - public async Task PutNickname([FromRoute][Username] string username, - [FromBody][StringLength(10, MinimumLength = 1)] string body) - { - await _service.SetNickname(username, body); - return Ok(); - } - - [HttpDelete("users/{username}/nickname")] - [Authorize] - [SelfOrAdmin] - [CatchUserNotExistException] - public async Task DeleteNickname([FromRoute][Username] string username) - { - await _service.SetNickname(username, null); - return Ok(); - } - } -} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 738440b2..ac4ad7b2 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -14,7 +14,7 @@ namespace Timeline.Entities protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); - modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); + modelBuilder.Entity().HasIndex(e => e.Username).IsUnique(); } public DbSet Users { get; set; } = default!; diff --git a/Timeline/Entities/UserDetailEntity.cs b/Timeline/Entities/UserDetailEntity.cs deleted file mode 100644 index 1d9957f9..00000000 --- a/Timeline/Entities/UserDetailEntity.cs +++ /dev/null @@ -1,21 +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(26)] - public string? Nickname { get; set; } - - [Column("user")] - public long UserId { get; set; } - - [ForeignKey(nameof(UserId))] - public UserEntity User { get; set; } = default!; - } -} diff --git a/Timeline/Entities/UserEntity.cs b/Timeline/Entities/UserEntity.cs index 83ef5621..dae6979f 100644 --- a/Timeline/Entities/UserEntity.cs +++ b/Timeline/Entities/UserEntity.cs @@ -17,21 +17,22 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } - [Column("name"), MaxLength(26), Required] - public string Name { get; set; } = default!; + [Column("username"), MaxLength(26), Required] + public string Username { get; set; } = default!; [Column("password"), Required] - public string EncryptedPassword { get; set; } = default!; + public string Password { get; set; } = default!; [Column("roles"), Required] - public string RoleString { get; set; } = default!; + public string Roles { get; set; } = default!; [Column("version"), Required] public long Version { get; set; } - public UserAvatarEntity? Avatar { get; set; } + [Column("nickname"), MaxLength(40)] + public string? Nickname { get; set; } - public UserDetailEntity? Detail { get; set; } + public UserAvatarEntity? Avatar { get; set; } public List Timelines { get; set; } = default!; diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index c0754071..d27b3c16 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -10,3 +10,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 69bfacf2..b3812f48 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -1,14 +1,10 @@ +using System; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; namespace Timeline.Models.Http { - public class User - { - public string Username { get; set; } = default!; - public bool Administrator { get; set; } - } - + [Obsolete("Remove this.")] public class UserPutRequest { [Required] @@ -17,6 +13,7 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; } } + [Obsolete("Remove this.")] public class UserPatchRequest { public string? Password { get; set; } diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs new file mode 100644 index 00000000..05395022 --- /dev/null +++ b/Timeline/Models/User.cs @@ -0,0 +1,19 @@ +using Timeline.Models.Validation; + +namespace Timeline.Models +{ + public class User + { + [Username] + public string? Username { get; set; } + public bool? Administrator { get; set; } + public string? Nickname { get; set; } + public string? AvatarUrl { get; set; } + + + #region secret + public string? Password { get; set; } + public long? Version { get; set; } + #endregion secret + } +} diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs deleted file mode 100644 index eff47329..00000000 --- a/Timeline/Models/UserInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Timeline.Models -{ - public class UserInfo - { - public long Id { get; set; } - public long Version { get; set; } - public string Username { get; set; } = default!; - public bool Administrator { get; set; } - } -} diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index a16f6f81..ead7dbef 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -20,24 +20,46 @@ namespace Timeline.Models.Validation (bool, string) Validate(object? value); } + public static class ValidatorExtensions + { + public static bool Validate(this IValidator validator, object? value, out string message) + { + if (validator == null) + throw new ArgumentNullException(nameof(validator)); + + var (r, m) = validator.Validate(value); + message = m; + return r; + } + } + /// /// Convenient base class for validator. /// /// The type of accepted value. /// /// Subclass should override to do the real validation. - /// This class will check the nullity and type of value. If value is null or not of type - /// it will return false and not call . + /// This class will check the nullity and type of value. + /// If value is null, it will pass or fail depending on . + /// If value is not null and not of type + /// it will fail and not call . + /// + /// is true by default. /// /// If you want some other behaviours, write the validator from scratch. /// public abstract class Validator : IValidator { + protected bool PermitNull { get; set; } = true; + public (bool, string) Validate(object? value) { if (value == null) { - return (false, ValidatorMessageNull); + if (PermitNull) + return (true, GetSuccessMessage()); + else + return (false, ValidatorMessageNull); } if (value is T v) diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs new file mode 100644 index 00000000..e7630d26 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PasswordValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PasswordValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.PasswordValidator", typeof(PasswordValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Password can't be empty.. + /// + internal static string MessageEmptyString { + get { + return ResourceManager.GetString("MessageEmptyString", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.resx b/Timeline/Resources/Models/Validation/PasswordValidator.resx new file mode 100644 index 00000000..f445cc75 --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Password can't be empty. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx new file mode 100644 index 00000000..9eab7b4e --- /dev/null +++ b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 密码不能为空。 + + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 0a3325d4..671c4b93 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -240,6 +240,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Password is of bad format.. + /// + internal static string PasswordBadFormatException { + get { + return ResourceManager.GetString("PasswordBadFormatException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The timeline with that name already exists.. /// @@ -303,15 +312,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The username is of bad format.. - /// - internal static string UsernameBadFormatException { - get { - return ResourceManager.GetString("UsernameBadFormatException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The username already exists.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index bc96248d..3ae14d4e 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -177,6 +177,9 @@ version claim does not exist. + + Password is of bad format. + The timeline with that name already exists. @@ -198,9 +201,6 @@ The use is not a member of the timeline. - - The username is of bad format. - The username already exists. diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs new file mode 100644 index 00000000..28a74a6c --- /dev/null +++ b/Timeline/Resources/Services/UserCache.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserCache { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserCache() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Found user info from cache. Entry: {0} .. + /// + internal static string LogGetCacheExist { + get { + return ResourceManager.GetString("LogGetCacheExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info not exist in cache. Id: {0} .. + /// + internal static string LogGetCacheNotExist { + get { + return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info remove in cache. Id: {0} .. + /// + internal static string LogRemoveCache { + get { + return ResourceManager.GetString("LogRemoveCache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User info set in cache. Entry: {0} .. + /// + internal static string LogSetCache { + get { + return ResourceManager.GetString("LogSetCache", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx new file mode 100644 index 00000000..1102108b --- /dev/null +++ b/Timeline/Resources/Services/UserCache.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Found user info from cache. Entry: {0} . + + + User info not exist in cache. Id: {0} . + + + User info remove in cache. Id: {0} . + + + User info set in cache. Entry: {0} . + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserManager.Designer.cs b/Timeline/Resources/Services/UserManager.Designer.cs new file mode 100644 index 00000000..424499f8 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserManager { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserManager() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserManager", typeof(UserManager).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A user has been created.. + /// + internal static string LogUserCreate { + get { + return ResourceManager.GetString("LogUserCreate", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserManager.resx b/Timeline/Resources/Services/UserManager.resx new file mode 100644 index 00000000..ecb89179 --- /dev/null +++ b/Timeline/Resources/Services/UserManager.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A user has been created. + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs index 2a04dded..1b85546d 100644 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -78,6 +78,42 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Password can't be empty.. + /// + internal static string ExceptionPasswordEmpty { + get { + return ResourceManager.GetString("ExceptionPasswordEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password can't be null or empty.. + /// + internal static string ExceptionPasswordNullOrEmpty { + get { + return ResourceManager.GetString("ExceptionPasswordNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username is of bad format, because {}.. + /// + internal static string ExceptionUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username can't be null or empty.. + /// + internal static string ExceptionUsernameNullOrEmpty { + get { + return ResourceManager.GetString("ExceptionUsernameNullOrEmpty", resourceCulture); + } + } + /// /// Looks up a localized string similar to A cache entry is created.. /// diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx index 3670d8f9..26221770 100644 --- a/Timeline/Resources/Services/UserService.resx +++ b/Timeline/Resources/Services/UserService.resx @@ -123,6 +123,18 @@ Old username is of bad format. + + Password can't be empty. + + + Password can't be null or empty. + + + Username is of bad format, because {}. + + + Username can't be null or empty. + A cache entry is created. diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index c5c96d8c..e77dd01a 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -27,7 +27,7 @@ namespace Timeline.Services if (!result) throw new UsernameBadFormatException(username, message); - var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync(); if (userId == 0) throw new UserNotExistException(username); return userId; diff --git a/Timeline/Services/PasswordBadFormatException.cs b/Timeline/Services/PasswordBadFormatException.cs new file mode 100644 index 00000000..2029ebb4 --- /dev/null +++ b/Timeline/Services/PasswordBadFormatException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Timeline.Services +{ + + [Serializable] + public class PasswordBadFormatException : Exception + { + public PasswordBadFormatException() : base(Resources.Services.Exception.PasswordBadFormatException) { } + public PasswordBadFormatException(string message) : base(message) { } + public PasswordBadFormatException(string message, Exception inner) : base(message, inner) { } + + public PasswordBadFormatException(string password, string validationMessage) : this() + { + Password = password; + ValidationMessage = validationMessage; + } + + protected PasswordBadFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public string Password { get; set; } = ""; + + public string ValidationMessage { get; set; } = ""; + } +} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f7b0e0e9..f43d2de5 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -356,7 +356,7 @@ namespace Timeline.Services { Id = entity.Id, Content = entity.Content, - Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name, + Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name, Time = entity.Time }); } @@ -382,7 +382,7 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name); - var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault(); + var authorEntity = Database.Users.Where(u => u.Username == author).Select(u => new { u.Id }).SingleOrDefault(); if (authorEntity == null) { throw new UserNotExistException(author); @@ -508,7 +508,7 @@ namespace Timeline.Services List result = new List(); foreach (var (username, index) in map) { - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { throw new TimelineMemberOperationUserException(index, operation, username, @@ -550,7 +550,7 @@ namespace Timeline.Services throw new UsernameBadFormatException(username); } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -596,7 +596,7 @@ namespace Timeline.Services } } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -632,7 +632,7 @@ namespace Timeline.Services } } - var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (user == null) { @@ -672,7 +672,7 @@ namespace Timeline.Services } } - var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); + var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); if (userEntity == null) { @@ -715,7 +715,7 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync(); - var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray(); + var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray(); var memberUsernames = await Task.WhenAll(memberUsernameTasks); diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs deleted file mode 100644 index 4f4a7942..00000000 --- a/Timeline/Services/UserDetailService.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using static Timeline.Resources.Services.UserDetailService; - -namespace Timeline.Services -{ - public interface IUserDetailService - { - /// - /// Get the nickname of the user with given username. - /// If the user does not set a nickname, the username is returned as the nickname. - /// - /// The username of the user to get nickname of. - /// The nickname of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user does not exist. - Task GetNickname(string username); - - /// - /// Set the nickname of the user with given username. - /// - /// The username of the user to set nickname of. - /// The nickname. Pass null to unset. - /// Thrown when is null. - /// Thrown when is not null but its length is bigger than 10. - /// Thrown when is of bad format. - /// Thrown when the user does not exist. - Task SetNickname(string username, string? nickname); - } - - public class UserDetailService : IUserDetailService - { - private readonly DatabaseContext _database; - - private readonly ILogger _logger; - - public UserDetailService(DatabaseContext database, ILogger logger) - { - _database = database; - _logger = logger; - } - - public async Task GetNickname(string username) - { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var nickname = _database.UserDetails.Where(d => d.UserId == userId).Select(d => new { d.Nickname }).SingleOrDefault()?.Nickname; - return nickname ?? username; - } - - public async Task SetNickname(string username, string? nickname) - { - if (nickname != null && nickname.Length > 10) - { - throw new ArgumentException(ExceptionNicknameTooLong, nameof(nickname)); - } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var userDetail = _database.UserDetails.Where(d => d.UserId == userId).SingleOrDefault(); - if (nickname == null) - { - if (userDetail == null || userDetail.Nickname == null) - { - return; - } - else - { - userDetail.Nickname = null; - await _database.SaveChangesAsync(); - _logger.LogInformation(LogEntityNicknameSetToNull, userId); - } - } - else - { - var create = userDetail == null; - if (create) - { - userDetail = new UserDetailEntity - { - UserId = userId - }; - } - userDetail!.Nickname = nickname; - if (create) - { - _database.UserDetails.Add(userDetail); - } - await _database.SaveChangesAsync(); - if (create) - { - _logger.LogInformation(LogEntityNicknameCreate, userId, nickname); - } - else - { - _logger.LogInformation(LogEntityNicknameSetNotNull, userId, nickname); - } - } - } - } -} diff --git a/Timeline/Services/UserNotExistException.cs b/Timeline/Services/UserNotExistException.cs index c7317f56..fd0b5ecf 100644 --- a/Timeline/Services/UserNotExistException.cs +++ b/Timeline/Services/UserNotExistException.cs @@ -31,11 +31,11 @@ namespace Timeline.Services /// /// The username of the user that does not exist. /// - public string? Username { get; set; } + public string Username { get; set; } = ""; /// /// The id of the user that does not exist. /// - public long? Id { get; set; } + public long Id { get; set; } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 104db1b0..c5595c99 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,13 +1,14 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; +using static Timeline.Resources.Services.UserService; namespace Timeline.Services { @@ -18,12 +19,12 @@ namespace Timeline.Services /// /// The username of the user to verify. /// The password of the user to verify. - /// The user info. + /// The user info and auth info. /// Thrown when or is null. - /// Thrown when username is of bad format. + /// Thrown when username is of bad format. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); + Task VerifyCredential(string username, string password); /// /// Try to get a user by id. @@ -31,7 +32,7 @@ namespace Timeline.Services /// The id of the user. /// The user info. /// Thrown when the user with given id does not exist. - Task GetUserById(long id); + Task GetUserById(long id); /// /// Get the user info of given username. @@ -39,30 +40,51 @@ namespace Timeline.Services /// Username of the user. /// The info of the user. /// Thrown when is null. - /// Thrown when is of bad format. + /// Thrown when is of bad format. /// Thrown when the user with given username does not exist. - Task GetUserByUsername(string username); + Task GetUserByUsername(string username); /// /// List all users. /// /// The user info of users. - Task ListUsers(); + Task ListUsers(); /// - /// Create or modify a user with given username. - /// Username must be match with [a-zA-z0-9-_]. + /// Create a user with given info. /// - /// Username of user. - /// Password of user. - /// Whether the user is administrator. - /// - /// Return if a new user is created. - /// Return if a existing user is modified. - /// - /// Thrown when or is null. - /// Thrown when is of bad format. - Task PutUser(string username, string password, bool administrator); + /// The info of new user. + /// The password, can't be null or empty. + /// The id of the new user. + /// Thrown when is null. + /// Thrown when some fields in is bad. + /// Thrown when a user with given username already exists. + /// + /// must not be null and must be a valid username. + /// must not be null or empty. + /// is false by default (null). + /// Other fields are ignored. + /// + Task CreateUser(User info); + + /// + /// Modify a user's info. + /// + /// The id of the user. + /// The new info. May be null. + /// Thrown when some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// can't be empty. + /// + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + Task ModifyUser(long id, User? info); /// /// Partially modify a user of given username. @@ -116,181 +138,164 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly IMemoryCache _memoryCache; private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; - _memoryCache = memoryCache; _databaseContext = databaseContext; _passwordService = passwordService; } - private static string GenerateCacheKeyByUserId(long id) => $"user:{id}"; - - private void RemoveCache(long id) + private void CheckUsernameFormat(string username, string? paramName, Func? messageBuilder = null) { - var key = GenerateCacheKeyByUserId(id); - _memoryCache.Remove(key); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheRemove, ("Key", key))); - } - - private void CheckUsernameFormat(string username, string? message = null) - { - var (result, validationMessage) = _usernameValidator.Validate(username); - if (!result) + if (!_usernameValidator.Validate(username, out var message)) { - if (message == null) - throw new UsernameBadFormatException(username, validationMessage); + if (messageBuilder == null) + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); else - throw new UsernameBadFormatException(username, validationMessage, message); + throw new ArgumentException(messageBuilder(message), paramName); } } - private static UserInfo CreateUserInfoFromEntity(UserEntity user) + private static User CreateUserFromEntity(UserEntity entity) { - return new UserInfo + return new User { - Id = user.Id, - Username = user.Name, - Administrator = UserRoleConvert.ToBool(user.RoleString), - Version = user.Version + Username = entity.Username, + Administrator = UserRoleConvert.ToBool(entity.Roles), + Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Version = entity.Version }; } - public async Task VerifyCredential(string username, string password) + public async Task VerifyCredential(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); if (password == null) throw new ArgumentNullException(nameof(password)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - // We need password info, so always check the database. - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) + if (entity == null) throw new UserNotExistException(username); - if (!_passwordService.VerifyPassword(user.EncryptedPassword, password)) + if (!_passwordService.VerifyPassword(entity.Password, password)) throw new BadPasswordException(password); - return CreateUserInfoFromEntity(user); + return CreateUserFromEntity(entity); } - public async Task GetUserById(long id) + public async Task GetUserById(long id) { - var key = GenerateCacheKeyByUserId(id); - if (!_memoryCache.TryGetValue(key, out var cache)) - { - // no cache, check the database - var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (user == null) - throw new UserNotExistException(id); + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - // create cache - cache = CreateUserInfoFromEntity(user); - _memoryCache.CreateEntry(key).SetValue(cache); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogCacheCreate, ("Key", key))); - } + if (user == null) + throw new UserNotExistException(id); - return cache; + return CreateUserFromEntity(user); } - public async Task GetUserByUsername(string username) + public async Task GetUserByUsername(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - var entity = await _databaseContext.Users - .Where(user => user.Name == username) - .SingleOrDefaultAsync(); + var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); if (entity == null) throw new UserNotExistException(username); - return CreateUserInfoFromEntity(entity); + return CreateUserFromEntity(entity); } - public async Task ListUsers() + public async Task ListUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); - return entities.Select(user => CreateUserInfoFromEntity(user)).ToArray(); + return entities.Select(user => CreateUserFromEntity(user)).ToArray(); } - public async Task PutUser(string username, string password, bool administrator) + public async Task CreateUser(User info) { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - CheckUsernameFormat(username); + if (info == null) + throw new ArgumentNullException(nameof(info)); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (string.IsNullOrEmpty(info.Username)) + throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info)); - if (user == null) - { - var newUser = new UserEntity - { - Name = username, - EncryptedPassword = _passwordService.HashPassword(password), - RoleString = UserRoleConvert.ToString(administrator), - Avatar = null - }; - await _databaseContext.AddAsync(newUser); - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseCreate, - ("Id", newUser.Id), ("Username", username), ("Administrator", administrator))); - return PutResult.Create; - } + CheckUsernameFormat(info.Username, nameof(info)); - user.EncryptedPassword = _passwordService.HashPassword(password); - user.RoleString = UserRoleConvert.ToString(administrator); - user.Version += 1; + if (string.IsNullOrEmpty(info.Password)) + throw new ArgumentException(ExceptionPasswordNullOrEmpty); + + var username = info.Username; + + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + + if (conflict) + throw new UsernameConfictException(username); + + var administrator = info.Administrator ?? false; + var password = info.Password; + + var newEntity = new UserEntity + { + Username = username, + Password = _passwordService.HashPassword(password), + Roles = UserRoleConvert.ToString(administrator), + Version = 1 + }; + _databaseContext.Users.Add(newEntity); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Username", username), ("Administrator", administrator))); - //clear cache - RemoveCache(user.Id); + _logger.LogInformation(Log.Format(LogDatabaseCreate, + ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); - return PutResult.Modify; + return newEntity.Id; } - public async Task PatchUser(string username, string? password, bool? administrator) + public async Task ModifyUser(long id, User? info) { - if (username == null) - throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + if (info != null && info.Password != null && info.Password.Length == 0) + throw new ArgumentException(ExceptionPasswordEmpty, nameof(info)); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(username); + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); - if (password != null) + if (info != null) { - user.EncryptedPassword = _passwordService.HashPassword(password); - } + var password = info.Password; + if (password != null) + { + entity.Password = _passwordService.HashPassword(password); + } - if (administrator != null) - { - user.RoleString = UserRoleConvert.ToString(administrator.Value); + var administrator = info.Administrator; + if (administrator.HasValue) + { + entity.Roles = UserRoleConvert.ToString(administrator.Value); + } + + var nickname = info.Nickname; + if (nickname != null) + { + entity.Nickname = nickname; + } } - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id)); + entity.Version += 1; - //clear cache - RemoveCache(user.Id); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } public async Task DeleteUser(string username) @@ -299,7 +304,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(username)); CheckUsernameFormat(username); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); @@ -309,7 +314,7 @@ namespace Timeline.Services ("Id", user.Id))); //clear cache - RemoveCache(user.Id); + await _cache.RemoveCache(user.Id); } public async Task ChangePassword(string username, string oldPassword, string newPassword) @@ -322,21 +327,21 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(newPassword)); CheckUsernameFormat(username); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); - var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); + var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword); if (!verifyResult) throw new BadPasswordException(oldPassword); - user.EncryptedPassword = _passwordService.HashPassword(newPassword); + user.Password = _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); + await _cache.RemoveCache(user.Id); } public async Task ChangeUsername(string oldUsername, string newUsername) @@ -348,20 +353,22 @@ namespace Timeline.Services CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat); CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat); - var user = await _databaseContext.Users.Where(u => u.Name == oldUsername).SingleOrDefaultAsync(); + var user = await _databaseContext.Users.Where(u => u.Username == oldUsername).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(oldUsername); - var conflictUser = await _databaseContext.Users.Where(u => u.Name == newUsername).SingleOrDefaultAsync(); + var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync(); if (conflictUser != null) throw new UsernameConfictException(newUsername); - user.Name = newUsername; + user.Username = newUsername; user.Version += 1; await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername))); - RemoveCache(user.Id); + await _cache.RemoveCache(user.Id); } + + } } diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index c3cb51c9..a2c2980d 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -8,7 +8,7 @@ namespace Timeline.Services public class UserTokenCreateResult { public string Token { get; set; } = default!; - public UserInfo User { get; set; } = default!; + public User User { get; set; } = default!; } public interface IUserTokenManager @@ -36,7 +36,7 @@ namespace Timeline.Services /// Thrown when the token is of bad version. /// Thrown when the token is of bad format. /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); + public Task VerifyToken(string token); } public class UserTokenManager : IUserTokenManager @@ -68,7 +68,7 @@ namespace Timeline.Services } - public async Task VerifyToken(string token) + public async Task VerifyToken(string token) { if (token == null) throw new ArgumentNullException(nameof(token)); diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs deleted file mode 100644 index ad0350b5..00000000 --- a/Timeline/Services/UsernameBadFormatException.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Thrown when username is of bad format. - /// - [Serializable] - public class UsernameBadFormatException : Exception - { - public UsernameBadFormatException() : base(Resources.Services.Exception.UsernameBadFormatException) { } - public UsernameBadFormatException(string message) : base(message) { } - public UsernameBadFormatException(string message, Exception inner) : base(message, inner) { } - - public UsernameBadFormatException(string username, string validationMessage) : this() { Username = username; ValidationMessage = validationMessage; } - - public UsernameBadFormatException(string username, string validationMessage, string message) : this(message) { Username = username; ValidationMessage = validationMessage; } - - protected UsernameBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// Username of bad format. - /// - public string Username { get; private set; } = ""; - - public string ValidationMessage { get; private set; } = ""; - } -} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 90588f70..195252d9 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -82,6 +82,11 @@ True Common.resx + + True + True + PasswordValidator.resx + True True @@ -102,11 +107,21 @@ True UserAvatarService.resx + + True + True + UserCache.resx + True True UserDetailService.resx + + True + True + UserManager.resx + True True @@ -152,6 +167,10 @@ ResXFileCodeGenerator Common.Designer.cs + + ResXFileCodeGenerator + PasswordValidator.Designer.cs + ResXFileCodeGenerator UsernameValidator.Designer.cs @@ -168,10 +187,18 @@ ResXFileCodeGenerator UserAvatarService.Designer.cs + + ResXFileCodeGenerator + UserCache.Designer.cs + ResXFileCodeGenerator UserDetailService.Designer.cs + + ResXFileCodeGenerator + UserManager.Designer.cs + ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3 From 401a0c86054711bf5ebdce7d7717c9b59bffc2fa Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 18:38:41 +0800 Subject: ... --- Timeline.Tests/Controllers/UserControllerTest.cs | 4 +- Timeline/Auth/MyAuthenticationHandler.cs | 4 +- Timeline/Controllers/UserController.cs | 4 +- Timeline/Models/User.cs | 1 + Timeline/Models/Validation/NicknameValidator.cs | 26 ++ Timeline/Resources/Messages.zh.resx | 189 --------------- Timeline/Resources/Models/Http/Common.zh.resx | 132 ----------- .../Validation/NicknameValidator.Designer.cs | 72 ++++++ .../Models/Validation/NicknameValidator.resx | 123 ++++++++++ .../Validation/PasswordValidator.Designer.cs | 72 ------ .../Models/Validation/PasswordValidator.resx | 123 ---------- .../Models/Validation/PasswordValidator.zh.resx | 123 ---------- .../Models/Validation/UsernameValidator.zh.resx | 129 ---------- .../Resources/Models/Validation/Validator.zh.resx | 129 ---------- Timeline/Resources/Services/Exception.Designer.cs | 18 +- Timeline/Resources/Services/Exception.resx | 6 +- .../Resources/Services/UserManager.Designer.cs | 72 ------ Timeline/Resources/Services/UserManager.resx | 123 ---------- .../Resources/Services/UserService.Designer.cs | 36 +-- Timeline/Resources/Services/UserService.resx | 18 +- Timeline/Services/ConfictException.cs | 21 ++ Timeline/Services/UserService.cs | 263 +++++++++++++-------- Timeline/Services/UserTokenException.cs | 3 - Timeline/Services/UserTokenManager.cs | 4 +- Timeline/Services/UsernameConfictException.cs | 25 -- Timeline/Startup.cs | 19 -- Timeline/Timeline.csproj | 17 +- 27 files changed, 454 insertions(+), 1302 deletions(-) create mode 100644 Timeline/Models/Validation/NicknameValidator.cs delete mode 100644 Timeline/Resources/Messages.zh.resx delete mode 100644 Timeline/Resources/Models/Http/Common.zh.resx create mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs create mode 100644 Timeline/Resources/Models/Validation/NicknameValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.resx delete mode 100644 Timeline/Resources/Models/Validation/PasswordValidator.zh.resx delete mode 100644 Timeline/Resources/Models/Validation/UsernameValidator.zh.resx delete mode 100644 Timeline/Resources/Models/Validation/Validator.zh.resx delete mode 100644 Timeline/Resources/Services/UserManager.Designer.cs delete mode 100644 Timeline/Resources/Services/UserManager.resx create mode 100644 Timeline/Services/ConfictException.cs delete mode 100644 Timeline/Services/UsernameConfictException.cs (limited to 'Timeline/Services/UserTokenManager.cs') diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 192d53dd..3890712a 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -40,7 +40,7 @@ namespace Timeline.Tests.Controllers new Models.User { Id = 1, Username = "aaa", Administrator = true, Version = 1 }, new Models.User { Id = 2, Username = "bbb", Administrator = false, Version = 1 } }; - _mockUserService.Setup(s => s.ListUsers()).ReturnsAsync(mockUserList); + _mockUserService.Setup(s => s.GetUsers()).ReturnsAsync(mockUserList); var action = await _controller.List(); action.Result.Should().BeAssignableTo() .Which.Value.Should().BeEquivalentTo( @@ -165,7 +165,7 @@ namespace Timeline.Tests.Controllers [Theory] [InlineData(typeof(UserNotExistException), ErrorCodes.UserCommon.NotExist)] - [InlineData(typeof(UsernameConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] + [InlineData(typeof(ConfictException), ErrorCodes.UserController.ChangeUsername_Conflict)] public async Task Op_ChangeUsername_Failure(Type exceptionType, int code) { const string oldUsername = "aaa"; diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index 5bae5117..e6b26c4b 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -82,9 +82,9 @@ namespace Timeline.Auth var userInfo = await _userTokenManager.VerifyToken(token); var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userInfo.Id!.Value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator!.Value).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); var principal = new ClaimsPrincipal(); principal.AddIdentity(identity); diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 5f1b7bd7..3305952a 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -29,7 +29,7 @@ namespace Timeline.Controllers [HttpGet("users"), AdminAuthorize] public async Task> List() { - return Ok(await _userService.ListUsers()); + return Ok(await _userService.GetUsers()); } [HttpGet("users/{username}"), AdminAuthorize] @@ -105,7 +105,7 @@ namespace Timeline.Controllers ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); return BadRequest(ErrorResponse.UserCommon.NotExist()); } - catch (UsernameConfictException e) + catch (ConfictException e) { _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs index 05395022..2cead892 100644 --- a/Timeline/Models/User.cs +++ b/Timeline/Models/User.cs @@ -12,6 +12,7 @@ namespace Timeline.Models #region secret + public long? Id { get; set; } public string? Password { get; set; } public long? Version { get; set; } #endregion secret diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs new file mode 100644 index 00000000..f6626a2a --- /dev/null +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -0,0 +1,26 @@ +using System; +using static Timeline.Resources.Models.Validation.NicknameValidator; + +namespace Timeline.Models.Validation +{ + public class NicknameValidator : Validator + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base.")] + protected override (bool, string) DoValidate(string value) + { + if (value.Length > 10) + return (false, MessageTooLong); + + return (true, GetSuccessMessage()); + } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] + public class NicknameAttribute : ValidateWithAttribute + { + public NicknameAttribute() : base(typeof(NicknameValidator)) + { + + } + } +} diff --git a/Timeline/Resources/Messages.zh.resx b/Timeline/Resources/Messages.zh.resx deleted file mode 100644 index 6e52befd..00000000 --- a/Timeline/Resources/Messages.zh.resx +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 请求体太大。它不能超过{0}. - - - 实际的请求体长度比头中指示的大。 - - - 实际的请求体长度比头中指示的小。 - - - 你没有权限做此操作。 - - - 请求头Content-Length缺失或者格式不对。 - - - 请求头Content-Length不能为0。 - - - 请求头Content-Type缺失。 - - - 请求头If-Non-Match格式不对。 - - - 请求模型格式不对。 - - - 第{0}个做{1}操作的用户名格式错误。 - - - 第{0}个做{1}操作的用户不存在。 - - - 要删除的消息不存在。 - - - 用户名或密码错误。 - - - 符号格式错误。这个符号可能不是这个服务器创建的。 - - - 符号是一个旧版本。用户可能已经更新了信息。 - - - 符号过期了。 - - - 用户不存在。管理员可能已经删除了这个用户。 - - - 图片不是正方形。 - - - 解码图片失败。 - - - 图片格式与请求头中指示的不一样。 - - - 要操作的用户不存在。 - - - 旧密码错误。 - - - 新用户名已经存在。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx deleted file mode 100644 index de74ac3b..00000000 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 删除了一个项目。 - - - 要删除的项目不存在,什么都没有修改。 - - - 创建了一个新项目。 - - - 修改了一个已存在的项目。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs b/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs new file mode 100644 index 00000000..522f305a --- /dev/null +++ b/Timeline/Resources/Models/Validation/NicknameValidator.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Models.Validation { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class NicknameValidator { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal NicknameValidator() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.NicknameValidator", typeof(NicknameValidator).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Nickname is too long.. + /// + internal static string MessageTooLong { + get { + return ResourceManager.GetString("MessageTooLong", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Models/Validation/NicknameValidator.resx b/Timeline/Resources/Models/Validation/NicknameValidator.resx new file mode 100644 index 00000000..b191b505 --- /dev/null +++ b/Timeline/Resources/Models/Validation/NicknameValidator.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nickname is too long. + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs b/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs deleted file mode 100644 index e7630d26..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Models.Validation { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class PasswordValidator { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PasswordValidator() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Models.Validation.PasswordValidator", typeof(PasswordValidator).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Password can't be empty.. - /// - internal static string MessageEmptyString { - get { - return ResourceManager.GetString("MessageEmptyString", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.resx b/Timeline/Resources/Models/Validation/PasswordValidator.resx deleted file mode 100644 index f445cc75..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Password can't be empty. - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx b/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx deleted file mode 100644 index 9eab7b4e..00000000 --- a/Timeline/Resources/Models/Validation/PasswordValidator.zh.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 密码不能为空。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx b/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx deleted file mode 100644 index 89d519b0..00000000 --- a/Timeline/Resources/Models/Validation/UsernameValidator.zh.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 空字符串是不允许的。 - - - 无效的字符,只能使用字母、数字、下划线和连字符。 - - - 太长了,不能大于26个字符。 - - \ No newline at end of file diff --git a/Timeline/Resources/Models/Validation/Validator.zh.resx b/Timeline/Resources/Models/Validation/Validator.zh.resx deleted file mode 100644 index 2f98e7e3..00000000 --- a/Timeline/Resources/Models/Validation/Validator.zh.resx +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 值不是类型{0}的实例。 - - - 值不能为null. - - - 验证成功。 - - \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 671c4b93..cada1788 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -114,6 +114,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to A present resource conflicts with the given resource.. + /// + internal static string ConfictException { + get { + return ResourceManager.GetString("ConfictException", resourceCulture); + } + } + /// /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server.. /// @@ -312,15 +321,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The username already exists.. - /// - internal static string UsernameConfictException { - get { - return ResourceManager.GetString("UsernameConfictException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 3ae14d4e..2cb0f11a 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -135,6 +135,9 @@ The password is wrong. + + A present resource conflicts with the given resource. + The hashes password is of bad format. It might not be created by server. @@ -201,9 +204,6 @@ The use is not a member of the timeline. - - The username already exists. - The user does not exist. diff --git a/Timeline/Resources/Services/UserManager.Designer.cs b/Timeline/Resources/Services/UserManager.Designer.cs deleted file mode 100644 index 424499f8..00000000 --- a/Timeline/Resources/Services/UserManager.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserManager { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserManager() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserManager", typeof(UserManager).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to A user has been created.. - /// - internal static string LogUserCreate { - get { - return ResourceManager.GetString("LogUserCreate", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserManager.resx b/Timeline/Resources/Services/UserManager.resx deleted file mode 100644 index ecb89179..00000000 --- a/Timeline/Resources/Services/UserManager.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - A user has been created. - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserService.Designer.cs b/Timeline/Resources/Services/UserService.Designer.cs index 1b85546d..cdf7f390 100644 --- a/Timeline/Resources/Services/UserService.Designer.cs +++ b/Timeline/Resources/Services/UserService.Designer.cs @@ -69,6 +69,15 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Nickname is of bad format, because {}.. + /// + internal static string ExceptionNicknameBadFormat { + get { + return ResourceManager.GetString("ExceptionNicknameBadFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Old username is of bad format.. /// @@ -88,11 +97,11 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Password can't be null or empty.. + /// Looks up a localized string similar to Password can't be null.. /// - internal static string ExceptionPasswordNullOrEmpty { + internal static string ExceptionPasswordNull { get { - return ResourceManager.GetString("ExceptionPasswordNullOrEmpty", resourceCulture); + return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture); } } @@ -106,29 +115,20 @@ namespace Timeline.Resources.Services { } /// - /// Looks up a localized string similar to Username can't be null or empty.. - /// - internal static string ExceptionUsernameNullOrEmpty { - get { - return ResourceManager.GetString("ExceptionUsernameNullOrEmpty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A cache entry is created.. + /// Looks up a localized string similar to A user with given username already exists.. /// - internal static string LogCacheCreate { + internal static string ExceptionUsernameConflict { get { - return ResourceManager.GetString("LogCacheCreate", resourceCulture); + return ResourceManager.GetString("ExceptionUsernameConflict", resourceCulture); } } /// - /// Looks up a localized string similar to A cache entry is removed.. + /// Looks up a localized string similar to Username can't be null.. /// - internal static string LogCacheRemove { + internal static string ExceptionUsernameNull { get { - return ResourceManager.GetString("LogCacheRemove", resourceCulture); + return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture); } } diff --git a/Timeline/Resources/Services/UserService.resx b/Timeline/Resources/Services/UserService.resx index 26221770..09bd4abb 100644 --- a/Timeline/Resources/Services/UserService.resx +++ b/Timeline/Resources/Services/UserService.resx @@ -120,26 +120,26 @@ New username is of bad format. + + Nickname is of bad format, because {}. + Old username is of bad format. Password can't be empty. - - Password can't be null or empty. + + Password can't be null. Username is of bad format, because {}. - - Username can't be null or empty. - - - A cache entry is created. + + A user with given username already exists. - - A cache entry is removed. + + Username can't be null. A new user entry is added to the database. diff --git a/Timeline/Services/ConfictException.cs b/Timeline/Services/ConfictException.cs new file mode 100644 index 00000000..dcd77366 --- /dev/null +++ b/Timeline/Services/ConfictException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Thrown when a resource already exists and conflicts with the given resource. + /// + /// + /// For example a username already exists and conflicts with the given username. + /// + [Serializable] + public class ConfictException : Exception + { + public ConfictException() : base(Resources.Services.Exception.ConfictException) { } + public ConfictException(string message) : base(message) { } + public ConfictException(string message, Exception inner) : base(message, inner) { } + protected ConfictException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index c5595c99..616e70ba 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -21,7 +21,7 @@ namespace Timeline.Services /// The password of the user to verify. /// The user info and auth info. /// Thrown when or is null. - /// Thrown when username is of bad format. + /// Thrown when is of bad format or is empty. /// Thrown when the user with given username does not exist. /// Thrown when password is wrong. Task VerifyCredential(string username, string password); @@ -48,7 +48,7 @@ namespace Timeline.Services /// List all users. /// /// The user info of users. - Task ListUsers(); + Task GetUsers(); /// /// Create a user with given info. @@ -58,11 +58,12 @@ namespace Timeline.Services /// The id of the new user. /// Thrown when is null. /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. + /// Thrown when a user with given username already exists. /// /// must not be null and must be a valid username. /// must not be null or empty. /// is false by default (null). + /// must be a valid nickname if set. It is empty by default. /// Other fields are ignored. /// Task CreateUser(User info); @@ -75,61 +76,70 @@ namespace Timeline.Services /// Thrown when some fields in is bad. /// Thrown when user with given id does not exist. /// - /// Only , and will be used. + /// Only , , and will be used. /// If null, then not change. /// Other fields are ignored. /// After modified, even if nothing is changed, version will increase. /// - /// can't be empty. + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. /// /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// + /// Task ModifyUser(long id, User? info); /// - /// Partially modify a user of given username. + /// Modify a user's info. + /// + /// The username of the user. + /// The new info. May be null. + /// Thrown when is null. + /// Thrown when is of bad format or some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. /// - /// Note that whether actually modified or not, Version of the user will always increase. + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + /// + Task ModifyUser(string username, User? info); + + /// + /// Delete a user of given id. /// - /// Username of the user to modify. Can't be null. - /// New password. Null if not modify. - /// Whether the user is administrator. Null if not modify. - /// Thrown if is null. - /// Thrown when is of bad format. - /// Thrown if the user with given username does not exist. - Task PatchUser(string username, string? password, bool? administrator); + /// Id of the user to delete. + /// True if user is deleted, false if user not exist. + Task DeleteUser(long id); /// /// Delete a user of given username. /// - /// Username of thet user to delete. Can't be null. + /// Username of the user to delete. Can't be null. + /// True if user is deleted, false if user not exist. /// Thrown if is null. - /// Thrown when is of bad format. - /// Thrown if the user with given username does not exist. - Task DeleteUser(string username); + /// Thrown when is of bad format. + Task DeleteUser(string username); /// /// Try to change a user's password with old password. /// - /// The name of user to change password of. - /// The user's old password. - /// The user's new password. - /// Thrown if or or is null. - /// Thrown when is of bad format. + /// The id of user to change password of. + /// Old password. + /// New password. + /// Thrown if or is null. + /// Thrown if or is empty. /// Thrown if the user with given username does not exist. /// Thrown if the old password is wrong. - Task ChangePassword(string username, string oldPassword, string newPassword); - - /// - /// Change a user's username. - /// - /// The user's old username. - /// The new username. - /// Thrown if or is null. - /// Thrown if the user with old username does not exist. - /// Thrown if the or is of bad format. - /// Thrown if user with the new username already exists. - Task ChangeUsername(string oldUsername, string newUsername); + Task ChangePassword(long id, string oldPassword, string newPassword); } public class UserService : IUserService @@ -138,11 +148,10 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - + private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; @@ -150,17 +159,35 @@ namespace Timeline.Services _passwordService = passwordService; } - private void CheckUsernameFormat(string username, string? paramName, Func? messageBuilder = null) + private void CheckUsernameFormat(string username, string? paramName) { if (!_usernameValidator.Validate(username, out var message)) { - if (messageBuilder == null) - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); - else - throw new ArgumentException(messageBuilder(message), paramName); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); + } + } + + private static void CheckPasswordFormat(string password, string? paramName) + { + if (password.Length == 0) + { + throw new ArgumentException(ExceptionPasswordEmpty, paramName); } } + private void CheckNicknameFormat(string nickname, string? paramName) + { + if (!_nicknameValidator.Validate(nickname, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); + } + } + + private static void ThrowUsernameConflict() + { + throw new ConfictException(ExceptionUsernameConflict); + } + private static User CreateUserFromEntity(UserEntity entity) { return new User @@ -168,6 +195,7 @@ namespace Timeline.Services Username = entity.Username, Administrator = UserRoleConvert.ToBool(entity.Roles), Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Id = entity.Id, Version = entity.Version }; } @@ -180,6 +208,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(password)); CheckUsernameFormat(username, nameof(username)); + CheckPasswordFormat(password, nameof(password)); var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); @@ -217,7 +246,7 @@ namespace Timeline.Services return CreateUserFromEntity(entity); } - public async Task ListUsers() + public async Task GetUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); return entities.Select(user => CreateUserFromEntity(user)).ToArray(); @@ -228,20 +257,22 @@ namespace Timeline.Services if (info == null) throw new ArgumentNullException(nameof(info)); - if (string.IsNullOrEmpty(info.Username)) - throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info)); - + if (info.Username == null) + throw new ArgumentException(ExceptionUsernameNull, nameof(info)); CheckUsernameFormat(info.Username, nameof(info)); - if (string.IsNullOrEmpty(info.Password)) - throw new ArgumentException(ExceptionPasswordNullOrEmpty); + if (info.Password == null) + throw new ArgumentException(ExceptionPasswordNull, nameof(info)); + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); var username = info.Username; var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); - if (conflict) - throw new UsernameConfictException(username); + ThrowUsernameConflict(); var administrator = info.Administrator ?? false; var password = info.Password; @@ -262,17 +293,35 @@ namespace Timeline.Services return newEntity.Id; } - public async Task ModifyUser(long id, User? info) + private void ValidateModifyUserInfo(User? info) { - if (info != null && info.Password != null && info.Password.Length == 0) - throw new ArgumentException(ExceptionPasswordEmpty, nameof(info)); + if (info != null) + { + if (info.Username != null) + CheckUsernameFormat(info.Username, nameof(info)); - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - if (entity == null) - throw new UserNotExistException(id); + if (info.Password != null) + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + } + } + private async Task UpdateUserEntity(UserEntity entity, User? info) + { if (info != null) { + var username = info.Username; + if (username != null) + { + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + entity.Username = username; + } + var password = info.Password; if (password != null) { @@ -293,82 +342,90 @@ namespace Timeline.Services } entity.Version += 1; + } + + + public async Task ModifyUser(long id, User? info) + { + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); + + await UpdateUserEntity(entity, info); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } - public async Task DeleteUser(string username) + public async Task ModifyUser(string username, User? info) { if (username == null) throw new ArgumentNullException(nameof(username)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); - var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - if (user == null) + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + if (entity == null) throw new UserNotExistException(username); - _databaseContext.Users.Remove(user); + await UpdateUserEntity(entity, info); + await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove, - ("Id", user.Id))); + _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); + } + + public async Task DeleteUser(long id) + { + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (user == null) + return false; - //clear cache - await _cache.RemoveCache(user.Id); + _databaseContext.Users.Remove(user); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username))); + return true; } - public async Task ChangePassword(string username, string oldPassword, string newPassword) + public async Task DeleteUser(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - CheckUsernameFormat(username); + CheckUsernameFormat(username, nameof(username)); var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) - throw new UserNotExistException(username); - - var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword); - if (!verifyResult) - throw new BadPasswordException(oldPassword); + return false; - user.Password = _passwordService.HashPassword(newPassword); - user.Version += 1; + _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Operation", "Change password"))); - //clear cache - await _cache.RemoveCache(user.Id); + _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username))); + return true; } - public async Task ChangeUsername(string oldUsername, string newUsername) + public async Task ChangePassword(long id, string oldPassword, string newPassword) { - 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.Username == oldUsername).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(oldUsername); + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + CheckPasswordFormat(oldPassword, nameof(oldPassword)); + CheckPasswordFormat(newPassword, nameof(newPassword)); - var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync(); - if (conflictUser != null) - throw new UsernameConfictException(newUsername); + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - user.Username = newUsername; - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, - ("Id", user.Id), ("Old Username", oldUsername), ("New Username", newUsername))); - await _cache.RemoveCache(user.Id); - } + if (entity == null) + throw new UserNotExistException(id); + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } } } diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs index e63305b1..ed0bae1a 100644 --- a/Timeline/Services/UserTokenException.cs +++ b/Timeline/Services/UserTokenException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Timeline.Services { diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs index a2c2980d..3e9ef3d4 100644 --- a/Timeline/Services/UserTokenManager.cs +++ b/Timeline/Services/UserTokenManager.cs @@ -62,7 +62,7 @@ namespace Timeline.Services throw new ArgumentNullException(nameof(password)); var user = await _userService.VerifyCredential(username, password); - var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); + var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id!.Value, Version = user.Version!.Value, ExpireAt = expireAt }); return new UserTokenCreateResult { Token = token, User = user }; } @@ -85,7 +85,7 @@ namespace Timeline.Services var user = await _userService.GetUserById(tokenInfo.Id); if (tokenInfo.Version < user.Version) - throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version); + throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version.Value); return user; } diff --git a/Timeline/Services/UsernameConfictException.cs b/Timeline/Services/UsernameConfictException.cs deleted file mode 100644 index fde1eda6..00000000 --- a/Timeline/Services/UsernameConfictException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Timeline.Helpers; - -namespace Timeline.Services -{ - /// - /// Thrown when the user already exists. - /// - [Serializable] - public class UsernameConfictException : Exception - { - public UsernameConfictException() : base(Resources.Services.Exception.UsernameConfictException) { } - public UsernameConfictException(string username) : base(Log.Format(Resources.Services.Exception.UsernameConfictException, ("Username", username))) { Username = username; } - public UsernameConfictException(string username, string message) : base(message) { Username = username; } - public UsernameConfictException(string message, Exception inner) : base(message, inner) { } - protected UsernameConfictException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - /// - /// The username that already exists. - /// - public string? Username { get; set; } - } -} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 379ce6ea..091a16e5 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,14 +1,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Localization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; -using System.Collections.Generic; -using System.Globalization; using System.Text.Json.Serialization; using Timeline.Auth; using Timeline.Configs; @@ -89,7 +86,6 @@ namespace Timeline services.AddTransient(); services.AddTransient(); services.AddUserAvatarService(); - services.AddScoped(); services.AddScoped(); @@ -113,8 +109,6 @@ namespace Timeline options.UseMySql(databaseConfig.ConnectionString); }); } - - services.AddMemoryCache(); } @@ -128,19 +122,6 @@ namespace Timeline app.UseRouting(); - var supportedCultures = new List - { - 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 195252d9..82b45094 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -82,10 +82,10 @@ True Common.resx - + True True - PasswordValidator.resx + NicknameValidator.resx True @@ -117,11 +117,6 @@ True UserDetailService.resx - - True - True - UserManager.resx - True True @@ -167,9 +162,9 @@ ResXFileCodeGenerator Common.Designer.cs - + ResXFileCodeGenerator - PasswordValidator.Designer.cs + NicknameValidator.Designer.cs ResXFileCodeGenerator @@ -195,10 +190,6 @@ ResXFileCodeGenerator UserDetailService.Designer.cs - - ResXFileCodeGenerator - UserManager.Designer.cs - ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3