using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
using static Timeline.Entities.UserUtility;
namespace Timeline.Services
{
public class CreateTokenResult
{
public string Token { get; set; }
public UserInfo User { get; set; }
}
[Serializable]
public class UserNotExistException : Exception
{
public UserNotExistException(): base("The user does not exist.") { }
public UserNotExistException(string message) : base(message) { }
public UserNotExistException(string message, Exception inner) : base(message, inner) { }
protected UserNotExistException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
[Serializable]
public class BadPasswordException : Exception
{
public BadPasswordException(): base("Password is wrong.") { }
public BadPasswordException(string message) : base(message) { }
public BadPasswordException(string message, Exception inner) : base(message, inner) { }
protected BadPasswordException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
[Serializable]
public class BadTokenVersionException : Exception
{
public BadTokenVersionException(): base("Token version is expired.") { }
public BadTokenVersionException(string message) : base(message) { }
public BadTokenVersionException(string message, Exception inner) : base(message, inner) { }
protected BadTokenVersionException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
public enum PutUserResult
{
///
/// A new user is created.
///
Created,
///
/// A existing user is modified.
///
Modified
}
public enum PatchUserResult
{
///
/// Succeed to modify user.
///
Success,
///
/// A user of given username does not exist.
///
NotExists
}
public enum DeleteUserResult
{
///
/// A existing user is deleted.
///
Deleted,
///
/// A user of given username does not exist.
///
NotExists
}
public enum ChangePasswordResult
{
///
/// Success to change password.
///
Success,
///
/// The user does not exists.
///
NotExists,
///
/// Old password is wrong.
///
BadOldPassword
}
public enum PutAvatarResult
{
///
/// Success to upload avatar.
///
Success,
///
/// The user does not exists.
///
UserNotExists
}
public interface IUserService
{
///
/// Try to anthenticate with the given username and password.
/// If success, create a token and return the user info.
///
/// The username of the user to anthenticate.
/// The password of the user to anthenticate.
/// An containing the created token and user info.
/// Thrown when or is null.
/// Thrown when the user with given username does not exist.
/// Thrown when password is wrong.
Task CreateToken(string username, string password);
///
/// Verify the given token.
/// If success, return the user info.
///
/// The token to verify.
/// The user info specified by the token.
/// Thrown when is null.
/// Thrown when the token is of bad format. Thrown by .
/// Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.
/// Thrown when the version in the token is expired. User needs to recreate the token.
Task VerifyToken(string token);
///
/// Get the user info of given username.
///
/// Username of the user.
/// The info of the user. Null if the user of given username does not exists.
Task GetUser(string username);
///
/// List all users.
///
/// The user info of users.
Task ListUsers();
///
/// Create or modify a user with given username.
/// Return if a new user is created.
/// Return if a existing user is modified.
///
/// Username of user.
/// Password of user.
/// Array of roles of user.
/// Return if a new user is created.
/// Return if a existing user is modified.
Task PutUser(string username, string password, bool isAdmin);
///
/// Partially modify a use of given username.
///
/// Username of the user to modify.
/// New password. If not modify, then null.
/// New roles. If not modify, then null.
/// Return if modification succeeds.
/// Return if the user of given username doesn't exist.
Task PatchUser(string username, string password, bool? isAdmin);
///
/// Delete a user of given username.
/// Return if the user is deleted.
/// Return if the user of given username
/// does not exist.
///
/// Username of thet user to delete.
/// if the user is deleted.
/// if the user doesn't 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.
/// if success.
/// if user does not exist.
/// if old password is wrong.
Task ChangePassword(string username, string oldPassword, string newPassword);
///
/// Get the true avatar url of a user.
///
/// The name of user.
/// The url if user exists. Null if user does not exist.
Task GetAvatarUrl(string username);
///
/// Put a avatar of a user.
///
/// The name of user.
/// The data of avatar image.
/// The mime type of the image.
/// Return if success.
/// Return if user does not exist.
Task PutAvatar(string username, byte[] data, string mimeType);
}
internal class UserCache
{
public string Username { get; set; }
public bool IsAdmin { get; set; }
public long Version { get; set; }
public UserInfo ToUserInfo()
{
return new UserInfo(Username, IsAdmin);
}
}
public class UserService : IUserService
{
private readonly ILogger _logger;
private readonly IMemoryCache _memoryCache;
private readonly DatabaseContext _databaseContext;
private readonly IJwtService _jwtService;
private readonly IPasswordService _passwordService;
private readonly IQCloudCosService _cosService;
public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService)
{
_logger = logger;
_memoryCache = memoryCache;
_databaseContext = databaseContext;
_jwtService = jwtService;
_passwordService = passwordService;
_cosService = cosService;
}
private string GenerateCacheKeyByUserId(long id) => $"user:{id}";
private void RemoveCache(long id)
{
_memoryCache.Remove(GenerateCacheKeyByUserId(id));
}
public async Task CreateToken(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
// We need password info, so always check the database.
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
{
var e = new UserNotExistException();
_logger.LogInformation(e, $"Create token failed. Reason: invalid username. Username = {username} Password = {password} .");
throw e;
}
if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
{
var e = new BadPasswordException();
_logger.LogInformation(e, $"Create token failed. Reason: invalid password. Username = {username} Password = {password} .");
throw e;
}
var token = _jwtService.GenerateJwtToken(new TokenInfo
{
Id = user.Id,
Version = user.Version
});
return new CreateTokenResult
{
Token = token,
User = CreateUserInfo(user)
};
}
public async Task VerifyToken(string token)
{
TokenInfo tokenInfo;
try
{
tokenInfo = _jwtService.VerifyJwtToken(token);
}
catch (JwtTokenVerifyException e)
{
_logger.LogInformation(e, $"Verify token falied. Reason: invalid token. Token: {token} .");
throw e;
}
var id = tokenInfo.Id;
var key = GenerateCacheKeyByUserId(id);
if (!_memoryCache.TryGetValue(key, out var cache))
{
// no cache, check the database
var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
if (user == null)
{
var e = new UserNotExistException();
_logger.LogInformation(e, $"Verify token falied. Reason: invalid id. Token: {token} Id: {id}.");
throw e;
}
// create cache
cache = CreateUserCache(user);
_memoryCache.CreateEntry(key).SetValue(cache);
}
if (tokenInfo.Version != cache.Version)
{
var e = new BadTokenVersionException();
_logger.LogInformation(e, $"Verify token falied. Reason: invalid version. Token: {token} Id: {id} Username: {cache.Username} Version: {tokenInfo.Version} Version in cache: {cache.Version}.");
throw e;
}
return cache.ToUserInfo();
}
public async Task GetUser(string username)
{
return await _databaseContext.Users
.Where(user => user.Name == username)
.Select(user => CreateUserInfo(user))
.SingleOrDefaultAsync();
}
public async Task ListUsers()
{
return await _databaseContext.Users
.Select(user => CreateUserInfo(user))
.ToArrayAsync();
}
public async Task PutUser(string username, string password, bool isAdmin)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
{
await _databaseContext.AddAsync(new User
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
RoleString = IsAdminToRoleString(isAdmin),
Version = 0
});
await _databaseContext.SaveChangesAsync();
return PutUserResult.Created;
}
user.EncryptedPassword = _passwordService.HashPassword(password);
user.RoleString = IsAdminToRoleString(isAdmin);
user.Version += 1;
await _databaseContext.SaveChangesAsync();
//clear cache
RemoveCache(user.Id);
return PutUserResult.Modified;
}
public async Task PatchUser(string username, string password, bool? isAdmin)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
return PatchUserResult.NotExists;
bool modified = false;
if (password != null)
{
modified = true;
user.EncryptedPassword = _passwordService.HashPassword(password);
}
if (isAdmin != null)
{
modified = true;
user.RoleString = IsAdminToRoleString(isAdmin.Value);
}
if (modified)
{
await _databaseContext.SaveChangesAsync();
//clear cache
RemoveCache(user.Id);
}
return PatchUserResult.Success;
}
public async Task DeleteUser(string username)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
{
return DeleteUserResult.NotExists;
}
_databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
//clear cache
RemoveCache(user.Id);
return DeleteUserResult.Deleted;
}
public async Task ChangePassword(string username, string oldPassword, string newPassword)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
return ChangePasswordResult.NotExists;
var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword);
if (!verifyResult)
return ChangePasswordResult.BadOldPassword;
user.EncryptedPassword = _passwordService.HashPassword(newPassword);
await _databaseContext.SaveChangesAsync();
//clear cache
RemoveCache(user.Id);
return ChangePasswordResult.Success;
}
public async Task GetAvatarUrl(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if ((await GetUser(username)) == null)
return null;
var exists = await _cosService.IsObjectExists("avatar", username);
if (exists)
return _cosService.GenerateObjectGetUrl("avatar", username);
else
return _cosService.GenerateObjectGetUrl("avatar", "__default");
}
public async Task PutAvatar(string username, byte[] data, string mimeType)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (data == null)
throw new ArgumentNullException(nameof(data));
if (mimeType == null)
throw new ArgumentNullException(nameof(mimeType));
if ((await GetUser(username)) == null)
return PutAvatarResult.UserNotExists;
await _cosService.PutObject("avatar", username, data, mimeType);
return PutAvatarResult.Success;
}
}
}