From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- BackEnd/Timeline/Services/UserService.cs | 437 +++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 BackEnd/Timeline/Services/UserService.cs (limited to 'BackEnd/Timeline/Services/UserService.cs') diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs new file mode 100644 index 00000000..821bc33d --- /dev/null +++ b/BackEnd/Timeline/Services/UserService.cs @@ -0,0 +1,437 @@ +using Microsoft.EntityFrameworkCore; +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 Timeline.Services.Exceptions; +using static Timeline.Resources.Services.UserService; + +namespace Timeline.Services +{ + public interface IUserService + { + /// + /// Try to verify the given username and password. + /// + /// The username of the user to verify. + /// The password of the user to verify. + /// The user info and auth info. + /// 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 get a user by id. + /// + /// The id of the user. + /// The user info. + /// Thrown when the user with given id does not exist. + Task GetUserById(long id); + + /// + /// Get the user info of given username. + /// + /// Username of the user. + /// The info of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserByUsername(string username); + + /// + /// 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); + + /// + /// List all users. + /// + /// The user info of users. + Task GetUsers(); + + /// + /// Create a user with given info. + /// + /// The info of new user. + /// The 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). + /// must be a valid nickname if set. It is empty by default. + /// Other fields are ignored. + /// + Task CreateUser(User info); + + /// + /// Modify a user's info. + /// + /// The id of the user. + /// The new info. May be null. + /// The new user info. + /// 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. + /// Version will increase if password is changed. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. + /// + /// + /// + Task ModifyUser(long id, User? info); + + /// + /// Modify a user's info. + /// + /// The username of the user. + /// The new info. May be null. + /// The new user info. + /// Thrown when is null. + /// Thrown when is of bad format or some fields in is bad. + /// Thrown when user with given id does not exist. + /// Thrown when user with the newusername already exist. + /// + /// Only , and will be used. + /// If null, then not change. + /// Other fields are ignored. + /// After modified, even if nothing is changed, version will increase. + /// + /// must be a valid username if set. + /// can't be empty if set. + /// must be a valid nickname if set. + /// + /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. + /// + /// + Task ModifyUser(string username, User? info); + + /// + /// 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 UserService : IUserService + { + private readonly ILogger _logger; + private readonly IClock _clock; + + private readonly DatabaseContext _databaseContext; + + private readonly IPasswordService _passwordService; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) + { + _logger = logger; + _clock = clock; + _databaseContext = databaseContext; + _passwordService = passwordService; + } + + private void CheckUsernameFormat(string username, string? paramName) + { + if (!_usernameValidator.Validate(username, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); + } + } + + private static void CheckPasswordFormat(string password, string? paramName) + { + if (password.Length == 0) + { + throw new ArgumentException(ExceptionPasswordEmpty, paramName); + } + } + + private void CheckNicknameFormat(string nickname, string? paramName) + { + if (!_nicknameValidator.Validate(nickname, out var message)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionNicknameBadFormat, message), paramName); + } + } + + private static void ThrowUsernameConflict() + { + throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); + } + + private static User CreateUserFromEntity(UserEntity entity) + { + return new User + { + UniqueId = entity.UniqueId, + Username = entity.Username, + Administrator = UserRoleConvert.ToBool(entity.Roles), + Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + Id = entity.Id, + Version = entity.Version, + CreateTime = entity.CreateTime, + UsernameChangeTime = entity.UsernameChangeTime, + LastModified = entity.LastModified + }; + } + + 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, nameof(username)); + CheckPasswordFormat(password, nameof(password)); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(entity.Password, password)) + throw new BadPasswordException(password); + + return CreateUserFromEntity(entity); + } + + public async Task GetUserById(long id) + { + var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (user == null) + throw new UserNotExistException(id); + + return CreateUserFromEntity(user); + } + + public async Task GetUserByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return CreateUserFromEntity(entity); + } + + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + + public async Task GetUsers() + { + var entities = await _databaseContext.Users.ToArrayAsync(); + return entities.Select(user => CreateUserFromEntity(user)).ToArray(); + } + + public async Task CreateUser(User info) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + if (info.Username == null) + throw new ArgumentException(ExceptionUsernameNull, nameof(info)); + CheckUsernameFormat(info.Username, nameof(info)); + + if (info.Password == null) + throw new ArgumentException(ExceptionPasswordNull, nameof(info)); + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + + var username = info.Username; + + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + var administrator = info.Administrator ?? false; + var password = info.Password; + var nickname = info.Nickname; + + var newEntity = new UserEntity + { + Username = username, + Password = _passwordService.HashPassword(password), + Roles = UserRoleConvert.ToString(administrator), + Nickname = nickname, + Version = 1 + }; + _databaseContext.Users.Add(newEntity); + await _databaseContext.SaveChangesAsync(); + + _logger.LogInformation(Log.Format(LogDatabaseCreate, + ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); + + return CreateUserFromEntity(newEntity); + } + + private void ValidateModifyUserInfo(User? info) + { + if (info != null) + { + if (info.Username != null) + CheckUsernameFormat(info.Username, nameof(info)); + + if (info.Password != null) + CheckPasswordFormat(info.Password, nameof(info)); + + if (info.Nickname != null) + CheckNicknameFormat(info.Nickname, nameof(info)); + } + } + + private async Task UpdateUserEntity(UserEntity entity, User? info) + { + if (info != null) + { + var now = _clock.GetCurrentTime(); + bool updateLastModified = false; + + var username = info.Username; + if (username != null && username != entity.Username) + { + var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); + if (conflict) + ThrowUsernameConflict(); + + entity.Username = username; + entity.UsernameChangeTime = now; + updateLastModified = true; + } + + var password = info.Password; + if (password != null) + { + entity.Password = _passwordService.HashPassword(password); + entity.Version += 1; + } + + var administrator = info.Administrator; + if (administrator.HasValue && UserRoleConvert.ToBool(entity.Roles) != administrator) + { + entity.Roles = UserRoleConvert.ToString(administrator.Value); + updateLastModified = true; + } + + var nickname = info.Nickname; + if (nickname != null && nickname != entity.Nickname) + { + entity.Nickname = nickname; + updateLastModified = true; + } + + if (updateLastModified) + { + entity.LastModified = now; + } + } + } + + + public async Task ModifyUser(long id, User? info) + { + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(id); + + await UpdateUserEntity(entity, info); + + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); + + return CreateUserFromEntity(entity); + } + + public async Task ModifyUser(string username, User? info) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + CheckUsernameFormat(username, nameof(username)); + + ValidateModifyUserInfo(info); + + var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); + if (entity == null) + throw new UserNotExistException(username); + + await UpdateUserEntity(entity, info); + + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(LogDatabaseUpdate, ("Username", username)); + + return CreateUserFromEntity(entity); + } + + 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 == null) + throw new UserNotExistException(id); + + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } + } +} -- cgit v1.2.3