From a6150c487e7a0eb3fb1d9874d2fa7de61cdbfd30 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Apr 2021 19:29:20 +0800 Subject: refactor: ... --- BackEnd/Timeline/Services/User/BasicUserService.cs | 39 +---- BackEnd/Timeline/Services/User/CreateUserParams.cs | 17 +++ .../Timeline/Services/User/IBasicUserService.cs | 36 +++++ .../Timeline/Services/User/IUserDeleteService.cs | 18 +++ .../Services/User/IUserPermissionService.cs | 35 +++++ BackEnd/Timeline/Services/User/IUserService.cs | 71 +++++++++ .../User/InvalidOperationOnRootUserException.cs | 2 +- BackEnd/Timeline/Services/User/ModifyUserParams.cs | 12 ++ BackEnd/Timeline/Services/User/PasswordService.cs | 2 +- .../Timeline/Services/User/Resource.Designer.cs | 81 ++++++++++ BackEnd/Timeline/Services/User/Resource.resx | 27 ++++ .../Timeline/Services/User/UserAvatarService.cs | 2 +- .../Services/User/UserCredentialService.cs | 101 ------------- .../Timeline/Services/User/UserDeleteService.cs | 16 +- BackEnd/Timeline/Services/User/UserPermission.cs | 18 +++ .../Services/User/UserPermissionService.cs | 166 +-------------------- BackEnd/Timeline/Services/User/UserPermissions.cs | 119 +++++++++++++++ BackEnd/Timeline/Services/User/UserService.cs | 148 +++++++++--------- 18 files changed, 518 insertions(+), 392 deletions(-) create mode 100644 BackEnd/Timeline/Services/User/CreateUserParams.cs create mode 100644 BackEnd/Timeline/Services/User/IBasicUserService.cs create mode 100644 BackEnd/Timeline/Services/User/IUserDeleteService.cs create mode 100644 BackEnd/Timeline/Services/User/IUserPermissionService.cs create mode 100644 BackEnd/Timeline/Services/User/IUserService.cs create mode 100644 BackEnd/Timeline/Services/User/ModifyUserParams.cs delete mode 100644 BackEnd/Timeline/Services/User/UserCredentialService.cs create mode 100644 BackEnd/Timeline/Services/User/UserPermission.cs create mode 100644 BackEnd/Timeline/Services/User/UserPermissions.cs (limited to 'BackEnd/Timeline/Services/User') diff --git a/BackEnd/Timeline/Services/User/BasicUserService.cs b/BackEnd/Timeline/Services/User/BasicUserService.cs index a3763ef6..1f1b25f5 100644 --- a/BackEnd/Timeline/Services/User/BasicUserService.cs +++ b/BackEnd/Timeline/Services/User/BasicUserService.cs @@ -7,37 +7,6 @@ using Timeline.Models.Validation; namespace Timeline.Services.User { - /// - /// This service provide some basic user features, which should be used internally for other services. - /// - public interface IBasicUserService - { - /// - /// Check if a user exists. - /// - /// The id of the user. - /// True if exists. Otherwise false. - Task CheckUserExistence(long id); - - /// - /// Get the user id of given username. - /// - /// Username of the user. - /// The id of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserIdByUsername(string username); - - /// - /// Get the username modified time of a user. - /// - /// User id. - /// The time. - /// Thrown when user does not exist. - Task GetUsernameLastModifiedTime(long userId); - } - public class BasicUserService : IBasicUserService { private readonly DatabaseContext _database; @@ -49,12 +18,12 @@ namespace Timeline.Services.User _database = database; } - public async Task CheckUserExistence(long id) + public async Task CheckUserExistenceAsync(long id) { return await _database.Users.AnyAsync(u => u.Id == id); } - public async Task GetUserIdByUsername(string username) + public async Task GetUserIdByUsernameAsync(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -70,7 +39,7 @@ namespace Timeline.Services.User return entity.Id; } - public async Task GetUsernameLastModifiedTime(long userId) + public async Task GetUsernameLastModifiedTimeAsync(long userId) { var entity = await _database.Users.Where(u => u.Id == userId).Select(u => new { u.UsernameChangeTime }).SingleOrDefaultAsync(); @@ -85,7 +54,7 @@ namespace Timeline.Services.User { public static async Task ThrowIfUserNotExist(this IBasicUserService service, long userId) { - if (!await service.CheckUserExistence(userId)) + if (!await service.CheckUserExistenceAsync(userId)) { throw new UserNotExistException(userId); } diff --git a/BackEnd/Timeline/Services/User/CreateUserParams.cs b/BackEnd/Timeline/Services/User/CreateUserParams.cs new file mode 100644 index 00000000..e66f83dc --- /dev/null +++ b/BackEnd/Timeline/Services/User/CreateUserParams.cs @@ -0,0 +1,17 @@ +using System; + +namespace Timeline.Services.User +{ + public class CreateUserParams + { + public CreateUserParams(string username, string password) + { + Username = username ?? throw new ArgumentNullException(nameof(username)); + Password = password ?? throw new ArgumentNullException(nameof(password)); + } + + public string Username { get; set; } + public string Password { get; set; } + public string? Nickname { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/User/IBasicUserService.cs b/BackEnd/Timeline/Services/User/IBasicUserService.cs new file mode 100644 index 00000000..0e30d733 --- /dev/null +++ b/BackEnd/Timeline/Services/User/IBasicUserService.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace Timeline.Services.User +{ + /// + /// This service provide some basic user features, which should be used internally for other services. + /// + public interface IBasicUserService + { + /// + /// Check if a user exists. + /// + /// The id of the user. + /// True if exists. Otherwise false. + Task CheckUserExistenceAsync(long id); + + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsernameAsync(string username); + + /// + /// Get the username modified time of a user. + /// + /// User id. + /// The time. + /// Thrown when user does not exist. + Task GetUsernameLastModifiedTimeAsync(long userId); + } +} diff --git a/BackEnd/Timeline/Services/User/IUserDeleteService.cs b/BackEnd/Timeline/Services/User/IUserDeleteService.cs new file mode 100644 index 00000000..ce9448ac --- /dev/null +++ b/BackEnd/Timeline/Services/User/IUserDeleteService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace Timeline.Services.User +{ + public interface IUserDeleteService + { + /// + /// Delete a user of given username. + /// + /// 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 when deleting root user. + Task DeleteUser(string username); + } +} diff --git a/BackEnd/Timeline/Services/User/IUserPermissionService.cs b/BackEnd/Timeline/Services/User/IUserPermissionService.cs new file mode 100644 index 00000000..7ff1275b --- /dev/null +++ b/BackEnd/Timeline/Services/User/IUserPermissionService.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +namespace Timeline.Services.User +{ + public interface IUserPermissionService + { + /// + /// Get permissions of a user. + /// + /// The id of the user. + /// Whether check the user's existence. + /// The permission list. + /// Thrown when is true and user does not exist. + Task GetPermissionsOfUserAsync(long userId, bool checkUserExistence = true); + + /// + /// Add a permission to user. + /// + /// The id of the user. + /// The new permission. + /// Thrown when user does not exist. + /// Thrown when change root user's permission. + Task AddPermissionToUserAsync(long userId, UserPermission permission); + + /// + /// Remove a permission from user. + /// + /// The id of the user. + /// The permission. + /// Whether check the user's existence. + /// Thrown when is true and user does not exist. + /// Thrown when change root user's permission. + Task RemovePermissionFromUserAsync(long userId, UserPermission permission, bool checkUserExistence = true); + } +} diff --git a/BackEnd/Timeline/Services/User/IUserService.cs b/BackEnd/Timeline/Services/User/IUserService.cs new file mode 100644 index 00000000..06155c55 --- /dev/null +++ b/BackEnd/Timeline/Services/User/IUserService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services.User +{ + public interface IUserService : IBasicUserService + { + /// + /// Try to get a user by id. + /// + /// The id of the user. + /// The user info. + /// Thrown when the user with given id does not exist. + Task GetUserAsync(long id); + + /// + /// List all users. + /// + /// The user info of users. + Task> GetUsersAsync(); + + /// + /// Create a user with given info. + /// + /// Info of new user. + /// The the new user. + /// Thrown when is null. + /// Thrown when param field is illegal. + /// Thrown when a user with given username already exists. + Task CreateUserAsync(CreateUserParams param); + + /// + /// Modify a user. + /// + /// The id of the user. + /// The new information. + /// The new user info. + /// Thrown when some fields in is bad. + /// Thrown when user with given id does not exist. + /// + /// Version will increase if password is changed. + /// + Task ModifyUserAsync(long id, ModifyUserParams? param); + + /// + /// Try to verify the given username and password. + /// + /// The username of the user to verify. + /// The password of the user to verify. + /// User id. + /// Thrown when or is null. + /// 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); + + /// + /// Try to change a user's password with old password. + /// + /// 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(long id, string oldPassword, string newPassword); + } +} diff --git a/BackEnd/Timeline/Services/User/InvalidOperationOnRootUserException.cs b/BackEnd/Timeline/Services/User/InvalidOperationOnRootUserException.cs index c432febd..636985d0 100644 --- a/BackEnd/Timeline/Services/User/InvalidOperationOnRootUserException.cs +++ b/BackEnd/Timeline/Services/User/InvalidOperationOnRootUserException.cs @@ -6,7 +6,7 @@ namespace Timeline.Services.User [Serializable] public class InvalidOperationOnRootUserException : InvalidOperationException { - public InvalidOperationOnRootUserException() { } + public InvalidOperationOnRootUserException() : base(Resource.ExceptionInvalidOperationOnRootUser) { } public InvalidOperationOnRootUserException(string message) : base(message) { } public InvalidOperationOnRootUserException(string message, Exception inner) : base(message, inner) { } protected InvalidOperationOnRootUserException( diff --git a/BackEnd/Timeline/Services/User/ModifyUserParams.cs b/BackEnd/Timeline/Services/User/ModifyUserParams.cs new file mode 100644 index 00000000..296c3212 --- /dev/null +++ b/BackEnd/Timeline/Services/User/ModifyUserParams.cs @@ -0,0 +1,12 @@ +namespace Timeline.Services.User +{ + /// + /// Null means not change. + /// + public class ModifyUserParams + { + public string? Username { get; set; } + public string? Password { get; set; } + public string? Nickname { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/User/PasswordService.cs b/BackEnd/Timeline/Services/User/PasswordService.cs index 1c14875f..5c2062dd 100644 --- a/BackEnd/Timeline/Services/User/PasswordService.cs +++ b/BackEnd/Timeline/Services/User/PasswordService.cs @@ -19,7 +19,7 @@ namespace Timeline.Services.User public HashedPasswordBadFromatException(string message, Exception inner) : base(message, inner) { } public HashedPasswordBadFromatException(string hashedPassword, string reason, Exception? inner = null) - : base(string.Format(CultureInfo.InvariantCulture, Resource.ExceptionHashedPasswordBadFormat, reason), inner) { HashedPassword = hashedPassword; } + : base(string.Format(CultureInfo.CurrentCulture, Resource.ExceptionHashedPasswordBadFormat, reason), inner) { HashedPassword = hashedPassword; } protected HashedPasswordBadFromatException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } diff --git a/BackEnd/Timeline/Services/User/Resource.Designer.cs b/BackEnd/Timeline/Services/User/Resource.Designer.cs index 4f75b055..908e2732 100644 --- a/BackEnd/Timeline/Services/User/Resource.Designer.cs +++ b/BackEnd/Timeline/Services/User/Resource.Designer.cs @@ -69,6 +69,24 @@ namespace Timeline.Services.User { } } + /// + /// Looks up a localized string similar to Can't change root user's permission.. + /// + internal static string ExceptionChangeRootUserPermission { + get { + return ResourceManager.GetString("ExceptionChangeRootUserPermission", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't delete root user.. + /// + internal static string ExceptionDeleteRootUser { + get { + return ResourceManager.GetString("ExceptionDeleteRootUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to The hashes password is of bad format. It might not be created by server. Reason: {0}. /// @@ -132,6 +150,15 @@ namespace Timeline.Services.User { } } + /// + /// Looks up a localized string similar to Can't perform such operation on root user.. + /// + internal static string ExceptionInvalidOperationOnRootUser { + get { + return ResourceManager.GetString("ExceptionInvalidOperationOnRootUser", resourceCulture); + } + } + /// /// Looks up a localized string similar to Nickname is of bad format. {0}. /// @@ -150,6 +177,15 @@ namespace Timeline.Services.User { } } + /// + /// Looks up a localized string similar to Password can't be null.. + /// + internal static string ExceptionPasswordNull { + get { + return ResourceManager.GetString("ExceptionPasswordNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to User with given constraints already exists.. /// @@ -168,6 +204,15 @@ namespace Timeline.Services.User { } } + /// + /// Looks up a localized string similar to Username can't be null.. + /// + internal static string ExceptionUsernameNull { + get { + return ResourceManager.GetString("ExceptionUsernameNull", resourceCulture); + } + } + /// /// Looks up a localized string similar to Requested user does not exist.. /// @@ -176,5 +221,41 @@ namespace Timeline.Services.User { return ResourceManager.GetString("ExceptionUserNotExist", resourceCulture); } } + + /// + /// Looks up a localized string similar to User with username = {0}, id ={1} changed password.. + /// + internal static string LogChangePassowrd { + get { + return ResourceManager.GetString("LogChangePassowrd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user is deleted with username = {0}, id = {1}.. + /// + internal static string LogDeleteUser { + get { + return ResourceManager.GetString("LogDeleteUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A new user is created with username = {0}, id = {1}.. + /// + internal static string LogUserCreated { + get { + return ResourceManager.GetString("LogUserCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user is modified with username = {0}, id = {1}.. + /// + internal static string LogUserModified { + get { + return ResourceManager.GetString("LogUserModified", resourceCulture); + } + } } } diff --git a/BackEnd/Timeline/Services/User/Resource.resx b/BackEnd/Timeline/Services/User/Resource.resx index 28e75b19..a734bd70 100644 --- a/BackEnd/Timeline/Services/User/Resource.resx +++ b/BackEnd/Timeline/Services/User/Resource.resx @@ -120,6 +120,12 @@ Password is wrong. + + Can't change root user's permission. + + + Can't delete root user. + The hashes password is of bad format. It might not be created by server. Reason: {0} @@ -141,19 +147,40 @@ Unknown format marker. + + Can't perform such operation on root user. + Nickname is of bad format. {0} Password can't be empty. + + Password can't be null. + User with given constraints already exists. Username is of bad format. {0} + + Username can't be null. + Requested user does not exist. + + User with username = {0}, id ={1} changed password. + + + A user is deleted with username = {0}, id = {1}. + + + A new user is created with username = {0}, id = {1}. + + + A user is modified with username = {0}, id = {1}. + \ No newline at end of file diff --git a/BackEnd/Timeline/Services/User/UserAvatarService.cs b/BackEnd/Timeline/Services/User/UserAvatarService.cs index e18a0560..9f59624d 100644 --- a/BackEnd/Timeline/Services/User/UserAvatarService.cs +++ b/BackEnd/Timeline/Services/User/UserAvatarService.cs @@ -144,7 +144,7 @@ namespace Timeline.Services.User public async Task GetAvatarDigest(long userId) { - var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTime(userId); + var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTimeAsync(userId); var entity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.DataTag, a.LastModified }).SingleOrDefaultAsync(); diff --git a/BackEnd/Timeline/Services/User/UserCredentialService.cs b/BackEnd/Timeline/Services/User/UserCredentialService.cs deleted file mode 100644 index 6becc469..00000000 --- a/BackEnd/Timeline/Services/User/UserCredentialService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Models.Validation; - -namespace Timeline.Services.User -{ - public interface IUserCredentialService - { - /// - /// Try to verify the given username and password. - /// - /// The username of the user to verify. - /// The password of the user to verify. - /// User id. - /// Thrown when or is null. - /// 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); - - /// - /// Try to change a user's password with old password. - /// - /// 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(long id, string oldPassword, string newPassword); - } - - public class UserCredentialService : IUserCredentialService - { - private readonly ILogger _logger; - private readonly DatabaseContext _database; - private readonly IPasswordService _passwordService; - - private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - - public UserCredentialService(ILogger logger, DatabaseContext database, IPasswordService passwordService) - { - _logger = logger; - _database = database; - _passwordService = passwordService; - } - - public async Task VerifyCredential(string username, string password) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - if (!_usernameValidator.Validate(username, out var message)) - throw new ArgumentException(message); - if (password.Length == 0) - throw new ArgumentException("Password can't be empty."); - - var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - if (!_passwordService.VerifyPassword(entity.Password, password)) - throw new BadPasswordException(password); - - return entity.Id; - } - - public async Task ChangePassword(long id, string oldPassword, string newPassword) - { - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - if (oldPassword.Length == 0) - throw new ArgumentException("Old password can't be empty."); - if (newPassword.Length == 0) - throw new ArgumentException("New password can't be empty."); - - var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - 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 _database.SaveChangesAsync(); - _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); - } - } -} diff --git a/BackEnd/Timeline/Services/User/UserDeleteService.cs b/BackEnd/Timeline/Services/User/UserDeleteService.cs index 8da4678a..94b15d33 100644 --- a/BackEnd/Timeline/Services/User/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/User/UserDeleteService.cs @@ -10,19 +10,6 @@ using Timeline.Services.Timeline; namespace Timeline.Services.User { - public interface IUserDeleteService - { - /// - /// Delete a user of given username. - /// - /// 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 when deleting root user. - Task DeleteUser(string username); - } - public class UserDeleteService : IUserDeleteService { private readonly ILogger _logger; @@ -55,13 +42,14 @@ namespace Timeline.Services.User return false; if (user.Id == 1) - throw new InvalidOperationOnRootUserException("Can't delete root user."); + throw new InvalidOperationOnRootUserException(Resource.ExceptionDeleteRootUser); await _timelinePostService.DeleteAllPostsOfUser(user.Id); _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); + _logger.LogWarning(Resource.LogDeleteUser, user.Username, user.Id); return true; } diff --git a/BackEnd/Timeline/Services/User/UserPermission.cs b/BackEnd/Timeline/Services/User/UserPermission.cs new file mode 100644 index 00000000..1404cdcd --- /dev/null +++ b/BackEnd/Timeline/Services/User/UserPermission.cs @@ -0,0 +1,18 @@ +namespace Timeline.Services.User +{ + public enum UserPermission + { + /// + /// This permission allows to manage user (creating, deleting or modifying). + /// + UserManagement, + /// + /// This permission allows to view and modify all timelines. + /// + AllTimelineManagement, + /// + /// This permission allow to add or remove highlight timelines. + /// + HighlightTimelineManagement + } +} diff --git a/BackEnd/Timeline/Services/User/UserPermissionService.cs b/BackEnd/Timeline/Services/User/UserPermissionService.cs index f292142d..f9911c7f 100644 --- a/BackEnd/Timeline/Services/User/UserPermissionService.cs +++ b/BackEnd/Timeline/Services/User/UserPermissionService.cs @@ -1,172 +1,10 @@ using Microsoft.EntityFrameworkCore; -using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; namespace Timeline.Services.User { - public enum UserPermission - { - /// - /// This permission allows to manage user (creating, deleting or modifying). - /// - UserManagement, - /// - /// This permission allows to view and modify all timelines. - /// - AllTimelineManagement, - /// - /// This permission allow to add or remove highlight timelines. - /// - HighlightTimelineManagement - } - - /// - /// Represents a user's permissions. - /// - public class UserPermissions : IEnumerable, IEquatable - { - public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues()); - - /// - /// Create an instance containing given permissions. - /// - /// Permission list. - public UserPermissions(params UserPermission[] permissions) : this(permissions as IEnumerable) - { - - } - - /// - /// Create an instance containing given permissions. - /// - /// Permission list. - /// Thrown when is null. - public UserPermissions(IEnumerable permissions) - { - if (permissions == null) throw new ArgumentNullException(nameof(permissions)); - _permissions = new SortedSet(permissions); - } - - private readonly SortedSet _permissions = new(); - - /// - /// Check if a permission is contained in the list. - /// - /// The permission to check. - /// True if contains. Otherwise false. - public bool Contains(UserPermission permission) - { - return _permissions.Contains(permission); - } - - /// - /// To a serializable string list. - /// - /// A string list. - public List ToStringList() - { - return _permissions.Select(p => p.ToString()).ToList(); - } - - /// - /// Convert a string list to user permissions. - /// - /// The string list. - /// An instance. - /// Thrown when is null. - /// Thrown when there is unknown permission name. - public static UserPermissions FromStringList(IEnumerable list) - { - List permissions = new(); - - foreach (var value in list) - { - if (Enum.TryParse(value, false, out var result)) - { - permissions.Add(result); - } - else - { - throw new ArgumentException("Unknown permission name.", nameof(list)); - } - } - - return new UserPermissions(permissions); - } - - public IEnumerator GetEnumerator() - { - return _permissions.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)_permissions).GetEnumerator(); - } - - public bool Equals(UserPermissions? other) - { - if (other == null) - return false; - - return _permissions.SequenceEqual(other._permissions); - } - - public override bool Equals(object? obj) - { - return Equals(obj as UserPermissions); - } - - public override int GetHashCode() - { - int result = 0; - foreach (var permission in Enum.GetValues()) - { - if (_permissions.Contains(permission)) - { - result += 1; - } - result <<= 1; - } - return result; - } - } - - public interface IUserPermissionService - { - /// - /// Get permissions of a user. - /// - /// The id of the user. - /// Whether check the user's existence. - /// The permission list. - /// Thrown when is true and user does not exist. - Task GetPermissionsOfUserAsync(long userId, bool checkUserExistence = true); - - /// - /// Add a permission to user. - /// - /// The id of the user. - /// The new permission. - /// Thrown when user does not exist. - /// Thrown when change root user's permission. - Task AddPermissionToUserAsync(long userId, UserPermission permission); - - /// - /// Remove a permission from user. - /// - /// The id of the user. - /// The permission. - /// Whether check the user's existence. - /// Thrown when is true and user does not exist. - /// Thrown when change root user's permission. - Task RemovePermissionFromUserAsync(long userId, UserPermission permission, bool checkUserExistence = true); - } - public class UserPermissionService : IUserPermissionService { private readonly DatabaseContext _database; @@ -205,7 +43,7 @@ namespace Timeline.Services.User public async Task AddPermissionToUserAsync(long userId, UserPermission permission) { if (userId == 1) - throw new InvalidOperationOnRootUserException("Can't change root user's permission."); + throw new InvalidOperationOnRootUserException(Resource.ExceptionChangeRootUserPermission); await CheckUserExistence(userId, true); @@ -222,7 +60,7 @@ namespace Timeline.Services.User public async Task RemovePermissionFromUserAsync(long userId, UserPermission permission, bool checkUserExistence = true) { if (userId == 1) - throw new InvalidOperationOnRootUserException("Can't change root user's permission."); + throw new InvalidOperationOnRootUserException(Resource.ExceptionChangeRootUserPermission); await CheckUserExistence(userId, checkUserExistence); diff --git a/BackEnd/Timeline/Services/User/UserPermissions.cs b/BackEnd/Timeline/Services/User/UserPermissions.cs new file mode 100644 index 00000000..1c27b4b8 --- /dev/null +++ b/BackEnd/Timeline/Services/User/UserPermissions.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Timeline.Services.User +{ + /// + /// Represents a user's permissions. + /// + public class UserPermissions : IEnumerable, IEquatable + { + public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues()); + + /// + /// Create an instance containing given permissions. + /// + /// Permission list. + public UserPermissions(params UserPermission[] permissions) : this(permissions as IEnumerable) + { + + } + + /// + /// Create an instance containing given permissions. + /// + /// Permission list. + /// Thrown when is null. + public UserPermissions(IEnumerable permissions) + { + if (permissions == null) throw new ArgumentNullException(nameof(permissions)); + _permissions = new SortedSet(permissions); + } + + private readonly SortedSet _permissions = new(); + + /// + /// Check if a permission is contained in the list. + /// + /// The permission to check. + /// True if contains. Otherwise false. + public bool Contains(UserPermission permission) + { + return _permissions.Contains(permission); + } + + /// + /// To a serializable string list. + /// + /// A string list. + public List ToStringList() + { + return _permissions.Select(p => p.ToString()).ToList(); + } + + /// + /// Convert a string list to user permissions. + /// + /// The string list. + /// An instance. + /// Thrown when is null. + /// Thrown when there is unknown permission name. + public static UserPermissions FromStringList(IEnumerable list) + { + List permissions = new(); + + foreach (var value in list) + { + if (Enum.TryParse(value, false, out var result)) + { + permissions.Add(result); + } + else + { + throw new ArgumentException("Unknown permission name.", nameof(list)); + } + } + + return new UserPermissions(permissions); + } + + public IEnumerator GetEnumerator() + { + return _permissions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_permissions).GetEnumerator(); + } + + public bool Equals(UserPermissions? other) + { + if (other == null) + return false; + + return _permissions.SequenceEqual(other._permissions); + } + + public override bool Equals(object? obj) + { + return Equals(obj as UserPermissions); + } + + public override int GetHashCode() + { + int result = 0; + foreach (var permission in Enum.GetValues()) + { + if (_permissions.Contains(permission)) + { + result += 1; + } + result <<= 1; + } + return result; + } + } +} diff --git a/BackEnd/Timeline/Services/User/UserService.cs b/BackEnd/Timeline/Services/User/UserService.cs index bbbe15b0..6496b55b 100644 --- a/BackEnd/Timeline/Services/User/UserService.cs +++ b/BackEnd/Timeline/Services/User/UserService.cs @@ -10,57 +10,6 @@ using Timeline.Models.Validation; namespace Timeline.Services.User { - /// - /// Null means not change. - /// - public class ModifyUserParams - { - public string? Username { get; set; } - public string? Password { get; set; } - public string? Nickname { get; set; } - } - - public interface IUserService : IBasicUserService - { - /// - /// Try to get a user by id. - /// - /// The id of the user. - /// The user info. - /// Thrown when the user with given id does not exist. - Task GetUser(long id); - - /// - /// List all users. - /// - /// The user info of users. - Task> GetUsers(); - - /// - /// Create a user with given info. - /// - /// The username of new user. - /// The password of new user. - /// The the new user. - /// Thrown when or is null. - /// Thrown when or is of bad format. - /// Thrown when a user with given username already exists. - Task CreateUser(string username, string password); - - /// - /// Modify a user. - /// - /// The id of the user. - /// The new information. - /// The new user info. - /// Thrown when some fields in is bad. - /// Thrown when user with given id does not exist. - /// - /// Version will increase if password is changed. - /// - Task ModifyUser(long id, ModifyUserParams? param); - } - public class UserService : BasicUserService, IUserService { private readonly ILogger _logger; @@ -110,58 +59,63 @@ namespace Timeline.Services.User throw new UserAlreadyExistException(user); } - public async Task GetUser(long id) + public async Task GetUserAsync(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - if (user == null) + if (user is null) throw new UserNotExistException(id); return user; } - public async Task> GetUsers() + public async Task> GetUsersAsync() { return await _databaseContext.Users.ToListAsync(); } - public async Task CreateUser(string username, string password) + public async Task CreateUserAsync(CreateUserParams param) { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - CheckUsernameFormat(username, nameof(username)); - CheckPasswordFormat(password, nameof(password)); - - var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (param is null) + throw new ArgumentNullException(nameof(param)); + if (param.Username is null) + throw new ArgumentException(Resource.ExceptionUsernameNull, nameof(param)); + if (param.Password is null) + throw new ArgumentException(Resource.ExceptionPasswordNull, nameof(param)); + CheckUsernameFormat(param.Username, nameof(param)); + CheckPasswordFormat(param.Password, nameof(param)); + if (param.Nickname is not null) + CheckNicknameFormat(param.Nickname, nameof(param)); + + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == param.Username); if (conflict) ThrowUsernameConflict(null); var newEntity = new UserEntity { - Username = username, - Password = _passwordService.HashPassword(password), + Username = param.Username, + Password = _passwordService.HashPassword(param.Password), + Nickname = param.Nickname, Version = 1 }; _databaseContext.Users.Add(newEntity); await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Resource.LogUserCreated, param.Username, newEntity.Id); return newEntity; } - public async Task ModifyUser(long id, ModifyUserParams? param) + public async Task ModifyUserAsync(long id, ModifyUserParams? param) { - if (param != null) + if (param is not null) { - if (param.Username != null) + if (param.Username is not null) CheckUsernameFormat(param.Username, nameof(param)); - if (param.Password != null) + if (param.Password is not null) CheckPasswordFormat(param.Password, nameof(param)); - if (param.Nickname != null) + if (param.Nickname is not null) CheckNicknameFormat(param.Nickname, nameof(param)); } @@ -169,13 +123,13 @@ namespace Timeline.Services.User if (entity == null) throw new UserNotExistException(id); - if (param != null) + if (param is not null) { var now = _clock.GetCurrentTime(); bool updateLastModified = false; var username = param.Username; - if (username != null && username != entity.Username) + if (username is not null && username != entity.Username) { var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); if (conflict) @@ -187,14 +141,14 @@ namespace Timeline.Services.User } var password = param.Password; - if (password != null) + if (password is not null) { entity.Password = _passwordService.HashPassword(password); entity.Version += 1; } var nickname = param.Nickname; - if (nickname != null && nickname != entity.Nickname) + if (nickname is not null && nickname != entity.Nickname) { entity.Nickname = nickname; updateLastModified = true; @@ -206,9 +160,53 @@ namespace Timeline.Services.User } await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Resource.LogUserModified, entity.Username, id); } return entity; } + + public async Task VerifyCredential(string username, string password) + { + if (username is null) + throw new ArgumentNullException(nameof(username)); + if (password is null) + throw new ArgumentNullException(nameof(password)); + CheckUsernameFormat(username, nameof(username)); + CheckPasswordFormat(password, nameof(password)); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync(); + + if (entity is null) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(entity.Password, password)) + throw new BadPasswordException(password); + + return entity.Id; + } + + public async Task ChangePassword(long id, string oldPassword, string newPassword) + { + 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 entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (entity is 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(Resource.LogChangePassowrd, entity.Username, id); + } } } -- cgit v1.2.3