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")));
}
}
}