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 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 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); /// /// 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); /// /// List all users. /// /// The user info of users. Task ListUsers(); /// /// 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). /// 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. /// /// Note that whether actually modified or not, Version of the user will always increase. /// /// Username of the user to modify. Can't be null. /// New password. Null if not modify. /// Whether the user is administrator. Null if not modify. /// Thrown if is null. /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. Task PatchUser(string username, string? password, bool? administrator); /// /// Delete a user of given username. /// /// Username of thet user to delete. Can't be null. /// Thrown if is null. /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. Task DeleteUser(string username); /// /// Try to change a user's password with old password. /// /// The name of user to change password of. /// The user's old password. /// The user's new password. /// Thrown if or or is null. /// Thrown when is of bad format. /// Thrown if the user with given username does not exist. /// Thrown if the old password is wrong. Task ChangePassword(string username, string oldPassword, string newPassword); /// /// Change a user's username. /// /// The user's old username. /// The new username. /// Thrown if or is null. /// Thrown if the user with old username does not exist. /// Thrown if the or is of bad format. /// Thrown if user with the new username already exists. Task ChangeUsername(string oldUsername, string newUsername); } public class UserService : IUserService { private readonly ILogger _logger; private readonly DatabaseContext _databaseContext; private readonly IPasswordService _passwordService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService) { _logger = logger; _databaseContext = databaseContext; _passwordService = passwordService; } private void CheckUsernameFormat(string username, string? paramName, Func? messageBuilder = null) { if (!_usernameValidator.Validate(username, out var message)) { if (messageBuilder == null) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionUsernameBadFormat, message), paramName); else throw new ArgumentException(messageBuilder(message), paramName); } } 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, 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)); 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 ListUsers() { 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 (string.IsNullOrEmpty(info.Username)) throw new ArgumentException(ExceptionUsernameNullOrEmpty, nameof(info)); CheckUsernameFormat(info.Username, nameof(info)); 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(LogDatabaseCreate, ("Id", newEntity.Id), ("Username", username), ("Administrator", administrator))); return newEntity.Id; } public async Task ModifyUser(long id, User? info) { if (info != null && info.Password != null && info.Password.Length == 0) throw new ArgumentException(ExceptionPasswordEmpty, nameof(info)); var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); if (entity == null) throw new UserNotExistException(id); if (info != null) { var password = info.Password; if (password != null) { entity.Password = _passwordService.HashPassword(password); } var administrator = info.Administrator; if (administrator.HasValue) { entity.Roles = UserRoleConvert.ToString(administrator.Value); } var nickname = info.Nickname; if (nickname != null) { entity.Nickname = nickname; } } entity.Version += 1; await _databaseContext.SaveChangesAsync(); _logger.LogInformation(LogDatabaseUpdate, ("Id", id)); } public async Task DeleteUser(string username) { if (username == null) throw new ArgumentNullException(nameof(username)); CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseRemove, ("Id", user.Id))); //clear cache await _cache.RemoveCache(user.Id); } public async Task ChangePassword(string username, string oldPassword, string newPassword) { if (username == null) throw new ArgumentNullException(nameof(username)); if (oldPassword == null) throw new ArgumentNullException(nameof(oldPassword)); if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); CheckUsernameFormat(username); var user = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(username); var verifyResult = _passwordService.VerifyPassword(user.Password, oldPassword); if (!verifyResult) throw new BadPasswordException(oldPassword); 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 await _cache.RemoveCache(user.Id); } public async Task ChangeUsername(string oldUsername, string newUsername) { if (oldUsername == null) throw new ArgumentNullException(nameof(oldUsername)); if (newUsername == null) throw new ArgumentNullException(nameof(newUsername)); CheckUsernameFormat(oldUsername, Resources.Services.UserService.ExceptionOldUsernameBadFormat); CheckUsernameFormat(newUsername, Resources.Services.UserService.ExceptionNewUsernameBadFormat); var user = await _databaseContext.Users.Where(u => u.Username == oldUsername).SingleOrDefaultAsync(); if (user == null) throw new UserNotExistException(oldUsername); var conflictUser = await _databaseContext.Users.Where(u => u.Username == newUsername).SingleOrDefaultAsync(); if (conflictUser != null) throw new UsernameConfictException(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))); await _cache.RemoveCache(user.Id); } } }