From 4eb1eb1a424b40adfa3bed79b9e58ce49c5a02c4 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 4 Aug 2019 23:26:23 +0800 Subject: Improve log. --- Timeline/Controllers/TokenController.cs | 73 ++- Timeline/Controllers/UserController.cs | 20 +- Timeline/Helpers/Log.cs | 24 + Timeline/Services/UserService.cs | 767 +++++++++++++++++--------------- 4 files changed, 492 insertions(+), 392 deletions(-) create mode 100644 Timeline/Helpers/Log.cs (limited to 'Timeline') diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 21f87ded..549e227b 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -1,10 +1,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using System; +using System.Collections.Generic; using System.Threading.Tasks; using Timeline.Entities.Http; using Timeline.Services; +using static Timeline.Helpers.MyLogHelper; namespace Timeline.Controllers { @@ -13,8 +16,8 @@ namespace Timeline.Controllers { private static class LoggingEventIds { - public const int LogInSucceeded = 1000; - public const int LogInFailed = 1001; + public const int CreateSucceeded = 1000; + public const int CreateFailed = 1001; public const int VerifySucceeded = 2000; public const int VerifyFailed = 2001; @@ -47,22 +50,36 @@ namespace Timeline.Controllers [AllowAnonymous] public async Task Create([FromBody] CreateTokenRequest request) { + void LogFailure(string reason, int code, Exception e = null) + { + _logger.LogInformation(LoggingEventIds.CreateFailed, e, FormatLogMessage("Attemp to login failed.", + Pair("Reason", reason), + Pair("Code", code), + Pair("Username", request.Username), + Pair("Password", request.Password), + Pair("Expire Offset (in days)", request.ExpireOffset))); + } + TimeSpan? expireOffset = null; if (request.ExpireOffset != null) { if (request.ExpireOffset.Value <= 0.0) { + const string message = "Expire time is not bigger than 0."; var code = ErrorCodes.Create_BadExpireOffset; - _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login failed because expire time offset is bad. Code: {} Username: {} Password: {} Bad Expire Offset: {}.", code, request.Username, request.Password, request.ExpireOffset); - return BadRequest(new CommonResponse(code, "Expire time is not bigger than 0.")); + LogFailure(message, code); + return BadRequest(new CommonResponse(code, message)); } expireOffset = TimeSpan.FromDays(request.ExpireOffset.Value); } try { - var result = await _userService.CreateToken(request.Username, request.Password, expireOffset == null ? null : (DateTime?)(_clock.GetCurrentTime() + expireOffset.Value)); - _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login succeeded. Username: {} Expire Time Offset: {} days.", request.Username, request.ExpireOffset); + var expiredTime = expireOffset == null ? null : (DateTime?)(_clock.GetCurrentTime() + expireOffset.Value); + var result = await _userService.CreateToken(request.Username, request.Password, expiredTime); + _logger.LogInformation(LoggingEventIds.CreateSucceeded, FormatLogMessage("Attemp to login succeeded.", + Pair("Username", request.Username), + Pair("Expire Time", expiredTime == null ? "default" : expiredTime.Value.ToString()))); return Ok(new CreateTokenResponse { Token = result.Token, @@ -72,13 +89,13 @@ namespace Timeline.Controllers catch (UserNotExistException e) { var code = ErrorCodes.Create_UserNotExist; - _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed because user does not exist. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); + LogFailure("User does not exist.", code, e); return BadRequest(new CommonResponse(code, "Bad username or password.")); } catch (BadPasswordException e) { var code = ErrorCodes.Create_BadPassword; - _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed because password is wrong. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); + LogFailure("Password is wrong.", code, e); return BadRequest(new CommonResponse(code, "Bad username or password.")); } } @@ -86,11 +103,23 @@ namespace Timeline.Controllers [HttpPost("verify")] [AllowAnonymous] public async Task Verify([FromBody] VerifyTokenRequest request) - { + { + void LogFailure(string reason, int code, Exception e = null, params KeyValuePair[] otherProperties) + { + var properties = new KeyValuePair[3 + otherProperties.Length]; + properties[0] = Pair("Reason", reason); + properties[1] = Pair("Code", code); + properties[2] = Pair("Token", request.Token); + otherProperties.CopyTo(properties, 3); + _logger.LogInformation(LoggingEventIds.VerifyFailed, e, FormatLogMessage("Token verification failed.", properties)); + } + try { var result = await _userService.VerifyToken(request.Token); - _logger.LogInformation(LoggingEventIds.VerifySucceeded, "Verify token succeeded. Username: {} Token: {} .", result.Username, request.Token); + _logger.LogInformation(LoggingEventIds.VerifySucceeded, + FormatLogMessage("Token verification succeeded.", + Pair("Username", result.Username), Pair("Token", request.Token))); return Ok(new VerifyTokenResponse { User = result @@ -100,28 +129,34 @@ namespace Timeline.Controllers { if (e.ErrorCode == JwtTokenVerifyException.ErrorCodes.Expired) { + const string message = "Token is expired."; var code = ErrorCodes.Verify_Expired; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a expired token. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonResponse(code, "A expired token.")); + var innerException = e.InnerException as SecurityTokenExpiredException; + LogFailure(message, code, e, Pair("Expires", innerException.Expires)); + return BadRequest(new CommonResponse(code, message)); } else { + const string message = "Token is of bad format."; var code = ErrorCodes.Verify_BadToken; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because of bad format. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonResponse(code, "A token of bad format.")); + LogFailure(message, code, e); + return BadRequest(new CommonResponse(code, message)); } } catch (UserNotExistException e) - { - var code = ErrorCodes.Verify_UserNotExist; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because user does not exist. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonResponse(code, "The user does not exist. Administrator might have deleted this user.")); + { + const string message = "User does not exist. Administrator might have deleted this user."; + var code = ErrorCodes.Verify_UserNotExist; + LogFailure(message, code, e); + return BadRequest(new CommonResponse(code, message)); } catch (BadTokenVersionException e) { + const string message = "Token has a old version."; var code = ErrorCodes.Verify_BadVersion; + LogFailure(message, code, e); _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because version is old. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonResponse(code, "The token is expired. Try recreate a token.")); + return BadRequest(new CommonResponse(code, message)); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 84267520..2099690c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -7,6 +7,7 @@ using Timeline.Authenticate; using Timeline.Entities; using Timeline.Entities.Http; using Timeline.Services; +using static Timeline.Helpers.MyLogHelper; namespace Timeline.Controllers { @@ -44,7 +45,7 @@ namespace Timeline.Controllers var user = await _userService.GetUser(username); if (user == null) { - _logger.LogInformation("Attempt to get a non-existent user. Username: {} .", username); + _logger.LogInformation(FormatLogMessage("Attempt to get a non-existent user.", Pair("Username", username))); return NotFound(new CommonResponse(ErrorCodes.Get_NotExists, "The user does not exist.")); } return Ok(user); @@ -53,7 +54,7 @@ namespace Timeline.Controllers [HttpPut("user/{username}"), AdminAuthorize] public async Task Put([FromBody] UserPutRequest request, [FromRoute] string username) { - if (request.Password == null) + if (request.Password == null) // This place will be refactored. { _logger.LogInformation("Attempt to put a user without a password. Username: {} .", username); return BadRequest(); @@ -63,10 +64,10 @@ namespace Timeline.Controllers switch (result) { case PutResult.Created: - _logger.LogInformation("Created a user. Username: {} .", username); + _logger.LogInformation(FormatLogMessage("A user is created.", Pair("Username", username))); return CreatedAtAction("Get", new { username }, CommonPutResponse.Created); case PutResult.Modified: - _logger.LogInformation("Modified a user. Username: {} .", username); + _logger.LogInformation(FormatLogMessage("A user is modified.", Pair("Username", username))); return Ok(CommonPutResponse.Modified); default: throw new Exception("Unreachable code."); @@ -83,7 +84,7 @@ namespace Timeline.Controllers } catch (UserNotExistException e) { - _logger.LogInformation(e, "Attempt to patch a non-existent user. Username: {} .", username); + _logger.LogInformation(e, FormatLogMessage("Attempt to patch a non-existent user.", Pair("Username", username))); return BadRequest(new CommonResponse(ErrorCodes.Patch_NotExists, "The user does not exist.")); } } @@ -94,12 +95,12 @@ namespace Timeline.Controllers try { await _userService.DeleteUser(username); - _logger.LogInformation("A user is deleted. Username: {} .", username); + _logger.LogInformation(FormatLogMessage("A user is deleted.", Pair("Username", username))); return Ok(CommonDeleteResponse.Deleted); } catch (UserNotExistException e) { - _logger.LogInformation(e, "Attempt to delete a non-existent user. Username: {} .", username); + _logger.LogInformation(e, FormatLogMessage("Attempt to delete a non-existent user.", Pair("Username", username))); return Ok(CommonDeleteResponse.NotExists); } } @@ -110,12 +111,13 @@ namespace Timeline.Controllers try { await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword); - _logger.LogInformation("A user changed password. Username: {} .", User.Identity.Name); + _logger.LogInformation(FormatLogMessage("A user changed password.", Pair("Username", User.Identity.Name))); return Ok(); } catch (BadPasswordException e) { - _logger.LogInformation(e, "A user attempt to change password but old password is wrong. Username: {} .", User.Identity.Name); + _logger.LogInformation(e, FormatLogMessage("A user attempt to change password but old password is wrong.", + Pair("Username", User.Identity.Name), Pair("Old Password", request.OldPassword))); return BadRequest(new CommonResponse(ErrorCodes.ChangePassword_BadOldPassword, "Old password is wrong.")); } // User can't be non-existent or the token is bad. diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs new file mode 100644 index 00000000..ac231810 --- /dev/null +++ b/Timeline/Helpers/Log.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Text; + +namespace Timeline.Helpers +{ + public static class MyLogHelper + { + public static KeyValuePair Pair(string key, object value) => new KeyValuePair(key, value); + + public static string FormatLogMessage(string summary, params KeyValuePair[] properties) + { + var builder = new StringBuilder(); + builder.Append(summary); + foreach (var property in properties) + { + builder.AppendLine(); + builder.Append(property.Key); + builder.Append(" : "); + builder.Append(property.Value); + } + return builder.ToString(); + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 0d6934ff..7fe7a2b6 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,369 +1,408 @@ -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 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. - /// The expired time point. Null then use default. See for what is default. - /// 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, DateTime? expires = null); - - /// - /// 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. - /// Whether the user is administrator. - /// Return if a new user is created. - /// Return if a existing user is modified. - /// Thrown when or is null. - Task PutUser(string username, string password, bool administrator); - - /// - /// 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 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 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 if the user with given username does not exist. - /// Thrown if the old password is wrong. - Task ChangePassword(string username, string oldPassword, string newPassword); - } - - internal class UserCache - { - public string Username { get; set; } - public bool Administrator { get; set; } - public long Version { get; set; } - - public UserInfo ToUserInfo() - { - return new UserInfo(Username, Administrator); - } - } - - public class UserService : IUserService - { - private readonly ILogger _logger; - - private readonly IMemoryCache _memoryCache; - private readonly DatabaseContext _databaseContext; - - private readonly IJwtService _jwtService; - private readonly IPasswordService _passwordService; - - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) - { - _logger = logger; - _memoryCache = memoryCache; - _databaseContext = databaseContext; - _jwtService = jwtService; - _passwordService = passwordService; - } - - private string GenerateCacheKeyByUserId(long id) => $"user:{id}"; - - private void RemoveCache(long id) - { - _memoryCache.Remove(GenerateCacheKeyByUserId(id)); - } - - public async Task CreateToken(string username, string password, DateTime? expires) - { - 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 - }, expires); - return new CreateTokenResult - { - Token = token, - User = CreateUserInfo(user) - }; - } - - public async Task VerifyToken(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(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 administrator) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - 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(administrator), - Version = 0 - }); - await _databaseContext.SaveChangesAsync(); - return PutResult.Created; - } - - user.EncryptedPassword = _passwordService.HashPassword(password); - user.RoleString = IsAdminToRoleString(administrator); - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - - //clear cache - RemoveCache(user.Id); - - return PutResult.Modified; - } - - public async Task PatchUser(string username, string password, bool? administrator) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(); - - if (password != null) - { - user.EncryptedPassword = _passwordService.HashPassword(password); - } - - if (administrator != null) - { - user.RoleString = IsAdminToRoleString(administrator.Value); +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; +using static Timeline.Helpers.MyLogHelper; + +namespace Timeline.Services +{ + public class CreateTokenResult + { + public string Token { get; set; } + public UserInfo User { get; set; } + } + + [Serializable] + public class UserNotExistException : Exception + { + private const string message = "The user does not exist."; + + public UserNotExistException(string username) + : base(FormatLogMessage(message, Pair("Username", username))) + { + Username = username; + } + + public UserNotExistException(long id) + : base(FormatLogMessage(message, Pair("Id", id))) + { + Id = id; + } + + public UserNotExistException(string message, Exception inner) : base(message, inner) { } + + protected UserNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The username that does not exist. May be null then is not null. + /// + public string Username { get; private set; } + + /// + /// The id that does not exist. May be null then is not null. + /// + public long? Id { get; private set; } + } + + [Serializable] + public class BadPasswordException : Exception + { + public BadPasswordException(string badPassword) + : base(FormatLogMessage("Password is wrong.", Pair("Bad Password", badPassword))) + { + Password = badPassword; + } + + public BadPasswordException(string message, Exception inner) : base(message, inner) { } + + protected BadPasswordException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The wrong password. + /// + public string Password { get; private set; } + } + + + [Serializable] + public class BadTokenVersionException : Exception + { + public BadTokenVersionException(long tokenVersion, long requiredVersion) + : base(FormatLogMessage("Token version is expired.", + Pair("Token Version", tokenVersion), + Pair("Required Version", requiredVersion))) + { + TokenVersion = tokenVersion; + RequiredVersion = requiredVersion; + } + + public BadTokenVersionException(string message, Exception inner) : base(message, inner) { } + + protected BadTokenVersionException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + /// + /// The version in the token. + /// + public long TokenVersion { get; private set; } + + /// + /// The version required. + /// + public long RequiredVersion { get; private set; } + } + + 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. + /// The expired time point. Null then use default. See for what is default. + /// 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, DateTime? expires = null); + + /// + /// 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. + /// Whether the user is administrator. + /// Return if a new user is created. + /// Return if a existing user is modified. + /// Thrown when or is null. + Task PutUser(string username, string password, bool administrator); + + /// + /// 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 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 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 if the user with given username does not exist. + /// Thrown if the old password is wrong. + Task ChangePassword(string username, string oldPassword, string newPassword); + } + + internal class UserCache + { + public string Username { get; set; } + public bool Administrator { get; set; } + public long Version { get; set; } + + public UserInfo ToUserInfo() + { + return new UserInfo(Username, Administrator); + } + } + + public class UserService : IUserService + { + private readonly ILogger _logger; + + private readonly IMemoryCache _memoryCache; + private readonly DatabaseContext _databaseContext; + + private readonly IJwtService _jwtService; + private readonly IPasswordService _passwordService; + + public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + { + _logger = logger; + _memoryCache = memoryCache; + _databaseContext = databaseContext; + _jwtService = jwtService; + _passwordService = passwordService; + } + + private string GenerateCacheKeyByUserId(long id) => $"user:{id}"; + + private void RemoveCache(long id) + { + var key = GenerateCacheKeyByUserId(id); + _memoryCache.Remove(key); + _logger.LogInformation(FormatLogMessage("A cache entry is removed.", Pair("Key", key))); + } + + public async Task CreateToken(string username, string password, DateTime? expires) + { + 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) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(user.EncryptedPassword, password)) + throw new BadPasswordException(password); + + var token = _jwtService.GenerateJwtToken(new TokenInfo + { + Id = user.Id, + Version = user.Version + }, expires); + + return new CreateTokenResult + { + Token = token, + User = CreateUserInfo(user) + }; + } + + public async Task VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + TokenInfo tokenInfo; + tokenInfo = _jwtService.VerifyJwtToken(token); + + 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) + throw new UserNotExistException(id); + + // create cache + cache = CreateUserCache(user); + _memoryCache.CreateEntry(key).SetValue(cache); + _logger.LogInformation(FormatLogMessage("A cache entry is created.", Pair("Key", key))); + } + + if (tokenInfo.Version != cache.Version) + throw new BadTokenVersionException(tokenInfo.Version, cache.Version); + + 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 administrator) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + + if (user == null) + { + var newUser = new User + { + Name = username, + EncryptedPassword = _passwordService.HashPassword(password), + RoleString = IsAdminToRoleString(administrator), + Version = 0 + }; + await _databaseContext.AddAsync(newUser); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(FormatLogMessage("A new user entry is added to the database.", Pair("Id", newUser.Id))); + return PutResult.Created; } + user.EncryptedPassword = _passwordService.HashPassword(password); + user.RoleString = IsAdminToRoleString(administrator); + user.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id))); + + //clear cache + RemoveCache(user.Id); + + return PutResult.Modified; + } + + public async Task PatchUser(string username, string password, bool? administrator) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + throw new UserNotExistException(username); + + if (password != null) + { + user.EncryptedPassword = _passwordService.HashPassword(password); + } + + if (administrator != null) + { + user.RoleString = IsAdminToRoleString(administrator.Value); + } + + user.Version += 1; + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(FormatLogMessage("A user entry is updated to the database.", Pair("Id", user.Id))); + + //clear cache + RemoveCache(user.Id); + } + + public async Task DeleteUser(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + throw new UserNotExistException(username); + + _databaseContext.Users.Remove(user); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation(FormatLogMessage("A user entry is removed from the database.", Pair("Id", user.Id))); + + //clear 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)); + + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + throw new UserNotExistException(username); + + var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); + if (!verifyResult) + throw new BadPasswordException(oldPassword); + + user.EncryptedPassword = _passwordService.HashPassword(newPassword); user.Version += 1; await _databaseContext.SaveChangesAsync(); //clear cache - RemoveCache(user.Id); - } - - public async Task DeleteUser(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(); - - _databaseContext.Users.Remove(user); - await _databaseContext.SaveChangesAsync(); - //clear 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)); - - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) - throw new UserNotExistException(); - - var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); - if (!verifyResult) - throw new BadPasswordException(); - - user.EncryptedPassword = _passwordService.HashPassword(newPassword); - user.Version += 1; - await _databaseContext.SaveChangesAsync(); - //clear cache - RemoveCache(user.Id); - } - } -} + RemoveCache(user.Id); + } + } +} -- cgit v1.2.3