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.Validation; 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 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). /// 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. /// 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. /// 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); /// /// Delete a user of given id. /// /// Id of the user to delete. /// True if user is deleted, false if user not exist. Task DeleteUser(long id); /// /// Delete a user of given username. /// /// Username of 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. Task DeleteUser(string username); /// /// 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 DatabaseContext _databaseContext; private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; _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 ConflictException(ExceptionUsernameConflict); } private static User CreateUserFromEntity(UserEntity entity) { return new User { Username = entity.Username, Administrator = UserRoleConvert.ToBool(entity.Roles), Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, Id = entity.Id, Version = entity.Version }; } 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 newEntity.Id; } 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 username = info.Username; if (username != null) { var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username); if (conflict) ThrowUsernameConflict(); entity.Username = username; } var password = info.Password; if (password != null) { entity.Password = _passwordService.HashPassword(password); entity.Version += 1; } var administrator = info.Administrator; if (administrator.HasValue) { entity.Roles = UserRoleConvert.ToString(administrator.Value); } var nickname = info.Nickname; if (nickname != null) { entity.Nickname = nickname; } } } public async Task ModifyUser(long id, User? info) { ValidateModifyUserInfo(info); var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); if (entity == null) throw new UserNotExistException(id); await UpdateUserEntity(entity, info); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } public async Task 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)); } public async Task DeleteUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); if (user == null) return false; _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", id), ("Username", user.Username))); return true; } public async Task DeleteUser(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); CheckUsernameFormat(username, nameof(username)); var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) return false; _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(LogDatabaseRemove, ("Id", user.Id), ("Username", username))); return true; } 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"))); } } }