From 747bf829351c30069647a44f98ac19f1a214370f Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 21 Jan 2020 01:11:17 +0800 Subject: ... --- Timeline/Models/Http/User.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'Timeline/Models/Http/User.cs') diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 516c1329..69bfacf2 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -3,6 +3,12 @@ using Timeline.Models.Validation; namespace Timeline.Models.Http { + public class User + { + public string Username { get; set; } = default!; + public bool Administrator { get; set; } + } + public class UserPutRequest { [Required] -- cgit v1.2.3 From 2750a3df1834c1c6623aa8c653504c7bc12cefd6 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/Models/Http/User.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 abde51b167f17301c25e32ae247e7909c0dc9367 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 29 Jan 2020 23:13:15 +0800 Subject: ... --- Timeline/Controllers/ControllerAuthExtensions.cs | 30 +++++++ Timeline/Controllers/UserController.cs | 99 +++++++++++++----------- Timeline/Models/Http/User.cs | 41 ---------- Timeline/Models/Http/UserController.cs | 27 +++++++ Timeline/Models/PutResult.cs | 17 ---- Timeline/Models/User.cs | 20 ----- Timeline/Models/UserRoleConvert.cs | 44 ----------- Timeline/Resources/Messages.Designer.cs | 36 +++++++++ Timeline/Resources/Messages.resx | 12 +++ Timeline/Services/User.cs | 49 ++++++++++++ Timeline/Services/UserRoleConvert.cs | 44 +++++++++++ Timeline/Services/UserService.cs | 1 - 12 files changed, 250 insertions(+), 170 deletions(-) create mode 100644 Timeline/Controllers/ControllerAuthExtensions.cs delete mode 100644 Timeline/Models/Http/User.cs create mode 100644 Timeline/Models/Http/UserController.cs delete mode 100644 Timeline/Models/PutResult.cs delete mode 100644 Timeline/Models/User.cs delete mode 100644 Timeline/Models/UserRoleConvert.cs create mode 100644 Timeline/Services/User.cs create mode 100644 Timeline/Services/UserRoleConvert.cs (limited to 'Timeline/Models/Http/User.cs') diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs new file mode 100644 index 00000000..81fd2428 --- /dev/null +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using Timeline.Auth; +using System; + +namespace Timeline.Controllers +{ + public static class ControllerAuthExtensions + { + public static bool IsAdministrator(this ControllerBase controller) + { + return controller.User != null && controller.User.IsAdministrator(); + } + + public static long GetUserId(this ControllerBase controller) + { + if (controller.User == null) + throw new InvalidOperationException("Failed to get user id because User is null."); + + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + } + } +} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 3305952a..4c585198 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,15 +1,16 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using System; +using System.Linq; using System.Threading.Tasks; using Timeline.Auth; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; using static Timeline.Resources.Controllers.UserController; +using static Timeline.Resources.Messages; namespace Timeline.Controllers { @@ -26,19 +27,20 @@ namespace Timeline.Controllers _userService = userService; } - [HttpGet("users"), AdminAuthorize] + [HttpGet("users")] public async Task> List() { - return Ok(await _userService.GetUsers()); + var users = await _userService.GetUsers(); + return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray()); } - [HttpGet("users/{username}"), AdminAuthorize] + [HttpGet("users/{username}")] public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(user); + return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator())); } catch (UserNotExistException e) { @@ -47,33 +49,53 @@ namespace Timeline.Controllers } } - [HttpPut("users/{username}"), AdminAuthorize] - public async Task> Put([FromBody] UserPutRequest request, [FromRoute][Username] string username) + [HttpPatch("users/{username}"), Authorize] + public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { - var result = await _userService.PutUser(username, request.Password, request.Administrator!.Value); - switch (result) + static User Convert(UserPatchRequest body) { - case PutResult.Create: - return CreatedAtAction("Get", new { username }, CommonPutResponse.Create()); - case PutResult.Modify: - return Ok(CommonPutResponse.Modify()); - default: - throw new Exception(ExceptionUnknownPutResult); + return new User + { + Username = body.Username, + Password = body.Password, + Administrator = body.Administrator, + Nickname = body.Nickname + }; } - } - [HttpPatch("users/{username}"), AdminAuthorize] - public async Task Patch([FromBody] UserPatchRequest request, [FromRoute][Username] string username) - { - try + if (this.IsAdministrator()) { - await _userService.PatchUser(username, request.Password, request.Administrator); - return Ok(); + try + { + await _userService.ModifyUser(username, Convert(body)); + return Ok(); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); + return NotFound(ErrorResponse.UserCommon.NotExist()); + } } - catch (UserNotExistException e) + else { - _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); - return NotFound(ErrorResponse.UserCommon.NotExist()); + if (User.Identity.Name != username) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(Common_Forbid_NotSelf)); + + if (body.Username != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Username)); + + if (body.Password != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Password)); + + if (body.Administrator != null) + return StatusCode(StatusCodes.Status403Forbidden, + ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); + + await _userService.ModifyUser(this.GetUserId(), Convert(body)); + return Ok(); } } @@ -91,27 +113,10 @@ namespace Timeline.Controllers } } - [HttpPost("userop/changeusername"), AdminAuthorize] - public async Task ChangeUsername([FromBody] ChangeUsernameRequest request) + [HttpPost("userop/create"), AdminAuthorize] + public async Task CreateUser([FromBody] User body) { - try - { - await _userService.ChangeUsername(request.OldUsername, request.NewUsername); - return Ok(); - } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameNotExist, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } - catch (ConfictException e) - { - _logger.LogInformation(e, Log.Format(LogChangeUsernameConflict, - ("Old Username", request.OldUsername), ("New Username", request.NewUsername))); - return BadRequest(ErrorResponse.UserController.ChangeUsername_Conflict()); - } - // there is no need to catch bad format exception because it is already checked in model validation. + } [HttpPost("userop/changepassword"), Authorize] @@ -119,7 +124,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(User.Identity.Name!, request.OldPassword, request.NewPassword); + await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); return Ok(); } catch (BadPasswordException e) diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs deleted file mode 100644 index b3812f48..00000000 --- a/Timeline/Models/Http/User.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Timeline.Models.Validation; - -namespace Timeline.Models.Http -{ - [Obsolete("Remove this.")] - public class UserPutRequest - { - [Required] - public string Password { get; set; } = default!; - [Required] - public bool? Administrator { get; set; } - } - - [Obsolete("Remove this.")] - public class UserPatchRequest - { - public string? Password { get; set; } - public bool? Administrator { get; set; } - } - - public class ChangeUsernameRequest - { - [Required] - [Username] - public string OldUsername { get; set; } = default!; - - [Required] - [Username] - public string NewUsername { get; set; } = default!; - } - - public class ChangePasswordRequest - { - [Required] - public string OldPassword { get; set; } = default!; - [Required] - public string NewPassword { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs new file mode 100644 index 00000000..229ca1e5 --- /dev/null +++ b/Timeline/Models/Http/UserController.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + public class UserPatchRequest + { + [Username] + public string? Username { get; set; } + + [MinLength(1)] + public string? Password { get; set; } + + [Nickname] + public string? Nickname { get; set; } + + public bool? Administrator { get; set; } + } + + public class ChangePasswordRequest + { + [Required(AllowEmptyStrings = false)] + public string OldPassword { get; set; } = default!; + [Required(AllowEmptyStrings = false)] + public string NewPassword { get; set; } = default!; + } +} diff --git a/Timeline/Models/PutResult.cs b/Timeline/Models/PutResult.cs deleted file mode 100644 index cecf86e6..00000000 --- a/Timeline/Models/PutResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Timeline.Models -{ - /// - /// Represents the result of a "put" operation. - /// - public enum PutResult - { - /// - /// Indicates the item did not exist and now is created. - /// - Create, - /// - /// Indicates the item exists already and is modified. - /// - Modify - } -} diff --git a/Timeline/Models/User.cs b/Timeline/Models/User.cs deleted file mode 100644 index 2cead892..00000000 --- a/Timeline/Models/User.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 long? Id { get; set; } - public string? Password { get; set; } - public long? Version { get; set; } - #endregion secret - } -} diff --git a/Timeline/Models/UserRoleConvert.cs b/Timeline/Models/UserRoleConvert.cs deleted file mode 100644 index ade9a799..00000000 --- a/Timeline/Models/UserRoleConvert.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Timeline.Entities; - -namespace Timeline.Models -{ - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] - public static class UserRoleConvert - { - public const string UserRole = UserRoles.User; - public const string AdminRole = UserRoles.Admin; - - public static string[] ToArray(bool administrator) - { - return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; - } - - public static string[] ToArray(string s) - { - return s.Split(',').ToArray(); - } - - public static bool ToBool(IReadOnlyCollection roles) - { - return roles.Contains(AdminRole); - } - - public static string ToString(IReadOnlyCollection roles) - { - return string.Join(',', roles); - } - - public static string ToString(bool administrator) - { - return administrator ? UserRole + "," + AdminRole : UserRole; - } - - public static bool ToBool(string s) - { - return s.Contains("admin", StringComparison.InvariantCulture); - } - } -} diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 8c13374f..15101661 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -96,6 +96,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to You are not the resource owner.. + /// + internal static string Common_Forbid_NotSelf { + get { + return ResourceManager.GetString("Common_Forbid_NotSelf", resourceCulture); + } + } + /// /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. /// @@ -266,5 +275,32 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); } } + + /// + /// Looks up a localized string similar to You can't set permission unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Administrator { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Administrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set password unless you are administrator. If you want to change password, use /userop/changepassword .. + /// + internal static string UserController_Patch_Forbid_Password { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't set username unless you are administrator.. + /// + internal static string UserController_Patch_Forbid_Username { + get { + return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index c5228ed5..db56ed02 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -129,6 +129,9 @@ You have no permission to do the operation. + + You are not the resource owner. + Header Content-Length is missing or of bad format. @@ -186,4 +189,13 @@ The new username already exists. + + You can't set permission unless you are administrator. + + + You can't set password unless you are administrator. If you want to change password, use /userop/changepassword . + + + You can't set username unless you are administrator. + \ No newline at end of file diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs new file mode 100644 index 00000000..f63a374e --- /dev/null +++ b/Timeline/Services/User.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using Timeline.Controllers; + +namespace Timeline.Services +{ + public class User + { + public string? Username { get; set; } + public string? Nickname { get; set; } + public string? AvatarUrl { get; set; } + + #region adminsecret + public bool? Administrator { get; set; } + #endregion adminsecret + + #region secret + public long? Id { get; set; } + public string? Password { get; set; } + public long? Version { get; set; } + #endregion secret + } + + public static class UserExtensions + { + public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + var result = new User + { + Username = user.Username, + Nickname = user.Nickname, + AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new + { + user.Username + }) + }; + + if (adminstrator) + { + result.Administrator = user.Administrator; + } + + return result; + } + } +} diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs new file mode 100644 index 00000000..4fa4a7b8 --- /dev/null +++ b/Timeline/Services/UserRoleConvert.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Timeline.Entities; + +namespace Timeline.Services +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] + public static class UserRoleConvert + { + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; + + public static string[] ToArray(bool administrator) + { + return administrator ? new string[] { UserRole, AdminRole } : new string[] { UserRole }; + } + + public static string[] ToArray(string s) + { + return s.Split(',').ToArray(); + } + + public static bool ToBool(IReadOnlyCollection roles) + { + return roles.Contains(AdminRole); + } + + public static string ToString(IReadOnlyCollection roles) + { + return string.Join(',', roles); + } + + public static string ToString(bool administrator) + { + return administrator ? UserRole + "," + AdminRole : UserRole; + } + + public static bool ToBool(string s) + { + return s.Contains("admin", StringComparison.InvariantCulture); + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 616e70ba..ff2306c5 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -6,7 +6,6 @@ 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; -- cgit v1.2.3