aboutsummaryrefslogtreecommitdiff
path: root/Timeline
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-08-04 23:26:23 +0800
committer杨宇千 <crupest@outlook.com>2019-08-04 23:26:23 +0800
commit4eb1eb1a424b40adfa3bed79b9e58ce49c5a02c4 (patch)
tree9016b0017634a7589981a38629f9b27ad76fb9ee /Timeline
parentebda3fc381ee4ed9f729fa85c1cee837ce4c5c3b (diff)
downloadtimeline-4eb1eb1a424b40adfa3bed79b9e58ce49c5a02c4.tar.gz
timeline-4eb1eb1a424b40adfa3bed79b9e58ce49c5a02c4.tar.bz2
timeline-4eb1eb1a424b40adfa3bed79b9e58ce49c5a02c4.zip
Improve log.
Diffstat (limited to 'Timeline')
-rw-r--r--Timeline/Controllers/TokenController.cs73
-rw-r--r--Timeline/Controllers/UserController.cs20
-rw-r--r--Timeline/Helpers/Log.cs24
-rw-r--r--Timeline/Services/UserService.cs767
4 files changed, 492 insertions, 392 deletions
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<IActionResult> 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<IActionResult> Verify([FromBody] VerifyTokenRequest request)
- {
+ {
+ void LogFailure(string reason, int code, Exception e = null, params KeyValuePair<string, object>[] otherProperties)
+ {
+ var properties = new KeyValuePair<string, object>[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<IActionResult> 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<string, object> Pair(string key, object value) => new KeyValuePair<string, object>(key, value);
+
+ public static string FormatLogMessage(string summary, params KeyValuePair<string, object>[] 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
- {
- /// <summary>
- /// Try to anthenticate with the given username and password.
- /// If success, create a token and return the user info.
- /// </summary>
- /// <param name="username">The username of the user to anthenticate.</param>
- /// <param name="password">The password of the user to anthenticate.</param>
- /// <param name="expires">The expired time point. Null then use default. See <see cref="JwtService.GenerateJwtToken(TokenInfo, DateTime?)"/> for what is default.</param>
- /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires = null);
-
- /// <summary>
- /// Verify the given token.
- /// If success, return the user info.
- /// </summary>
- /// <param name="token">The token to verify.</param>
- /// <returns>The user info specified by the token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
- /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
- Task<UserInfo> VerifyToken(string token);
-
- /// <summary>
- /// Get the user info of given username.
- /// </summary>
- /// <param name="username">Username of the user.</param>
- /// <returns>The info of the user. Null if the user of given username does not exists.</returns>
- Task<UserInfo> GetUser(string username);
-
- /// <summary>
- /// List all users.
- /// </summary>
- /// <returns>The user info of users.</returns>
- Task<UserInfo[]> ListUsers();
-
- /// <summary>
- /// Create or modify a user with given username.
- /// Return <see cref="PutUserResult.Created"/> if a new user is created.
- /// Return <see cref="PutUserResult.Modified"/> if a existing user is modified.
- /// </summary>
- /// <param name="username">Username of user.</param>
- /// <param name="password">Password of user.</param>
- /// <param name="administrator">Whether the user is administrator.</param>
- /// <returns>Return <see cref="PutResult.Created"/> if a new user is created.
- /// Return <see cref="PutResult.Modified"/> if a existing user is modified.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- Task<PutResult> PutUser(string username, string password, bool administrator);
-
- /// <summary>
- /// Partially modify a user of given username.
- ///
- /// Note that whether actually modified or not, Version of the user will always increase.
- /// </summary>
- /// <param name="username">Username of the user to modify. Can't be null.</param>
- /// <param name="password">New password. Null if not modify.</param>
- /// <param name="administrator">Whether the user is administrator. Null if not modify.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- Task PatchUser(string username, string password, bool? administrator);
-
- /// <summary>
- /// Delete a user of given username.
- /// </summary>
- /// <param name="username">Username of thet user to delete. Can't be null.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- Task DeleteUser(string username);
-
- /// <summary>
- /// Try to change a user's password with old password.
- /// </summary>
- /// <param name="username">The name of user to change password of.</param>
- /// <param name="oldPassword">The user's old password.</param>
- /// <param name="newPassword">The user's new password.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> or <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
- 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<UserService> _logger;
-
- private readonly IMemoryCache _memoryCache;
- private readonly DatabaseContext _databaseContext;
-
- private readonly IJwtService _jwtService;
- private readonly IPasswordService _passwordService;
-
- public UserService(ILogger<UserService> 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<CreateTokenResult> 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<UserInfo> 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<UserCache>(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<UserInfo> GetUser(string username)
- {
- return await _databaseContext.Users
- .Where(user => user.Name == username)
- .Select(user => CreateUserInfo(user))
- .SingleOrDefaultAsync();
- }
-
- public async Task<UserInfo[]> ListUsers()
- {
- return await _databaseContext.Users
- .Select(user => CreateUserInfo(user))
- .ToArrayAsync();
- }
-
- public async Task<PutResult> 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) { }
+
+ /// <summary>
+ /// The username that does not exist. May be null then <see cref="Id"/> is not null.
+ /// </summary>
+ public string Username { get; private set; }
+
+ /// <summary>
+ /// The id that does not exist. May be null then <see cref="Username"/> is not null.
+ /// </summary>
+ 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) { }
+
+ /// <summary>
+ /// The wrong password.
+ /// </summary>
+ 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) { }
+
+ /// <summary>
+ /// The version in the token.
+ /// </summary>
+ public long TokenVersion { get; private set; }
+
+ /// <summary>
+ /// The version required.
+ /// </summary>
+ public long RequiredVersion { get; private set; }
+ }
+
+ public interface IUserService
+ {
+ /// <summary>
+ /// Try to anthenticate with the given username and password.
+ /// If success, create a token and return the user info.
+ /// </summary>
+ /// <param name="username">The username of the user to anthenticate.</param>
+ /// <param name="password">The password of the user to anthenticate.</param>
+ /// <param name="expires">The expired time point. Null then use default. See <see cref="JwtService.GenerateJwtToken(TokenInfo, DateTime?)"/> for what is default.</param>
+ /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
+ Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires = null);
+
+ /// <summary>
+ /// Verify the given token.
+ /// If success, return the user info.
+ /// </summary>
+ /// <param name="token">The token to verify.</param>
+ /// <returns>The user info specified by the token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
+ /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
+ Task<UserInfo> VerifyToken(string token);
+
+ /// <summary>
+ /// Get the user info of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The info of the user. Null if the user of given username does not exists.</returns>
+ Task<UserInfo> GetUser(string username);
+
+ /// <summary>
+ /// List all users.
+ /// </summary>
+ /// <returns>The user info of users.</returns>
+ Task<UserInfo[]> ListUsers();
+
+ /// <summary>
+ /// Create or modify a user with given username.
+ /// Return <see cref="PutUserResult.Created"/> if a new user is created.
+ /// Return <see cref="PutUserResult.Modified"/> if a existing user is modified.
+ /// </summary>
+ /// <param name="username">Username of user.</param>
+ /// <param name="password">Password of user.</param>
+ /// <param name="administrator">Whether the user is administrator.</param>
+ /// <returns>Return <see cref="PutResult.Created"/> if a new user is created.
+ /// Return <see cref="PutResult.Modified"/> if a existing user is modified.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ Task<PutResult> PutUser(string username, string password, bool administrator);
+
+ /// <summary>
+ /// Partially modify a user of given username.
+ ///
+ /// Note that whether actually modified or not, Version of the user will always increase.
+ /// </summary>
+ /// <param name="username">Username of the user to modify. Can't be null.</param>
+ /// <param name="password">New password. Null if not modify.</param>
+ /// <param name="administrator">Whether the user is administrator. Null if not modify.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ Task PatchUser(string username, string password, bool? administrator);
+
+ /// <summary>
+ /// Delete a user of given username.
+ /// </summary>
+ /// <param name="username">Username of thet user to delete. Can't be null.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ Task DeleteUser(string username);
+
+ /// <summary>
+ /// Try to change a user's password with old password.
+ /// </summary>
+ /// <param name="username">The name of user to change password of.</param>
+ /// <param name="oldPassword">The user's old password.</param>
+ /// <param name="newPassword">The user's new password.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> or <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
+ 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<UserService> _logger;
+
+ private readonly IMemoryCache _memoryCache;
+ private readonly DatabaseContext _databaseContext;
+
+ private readonly IJwtService _jwtService;
+ private readonly IPasswordService _passwordService;
+
+ public UserService(ILogger<UserService> 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<CreateTokenResult> 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<UserInfo> 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<UserCache>(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<UserInfo> GetUser(string username)
+ {
+ return await _databaseContext.Users
+ .Where(user => user.Name == username)
+ .Select(user => CreateUserInfo(user))
+ .SingleOrDefaultAsync();
+ }
+
+ public async Task<UserInfo[]> ListUsers()
+ {
+ return await _databaseContext.Users
+ .Select(user => CreateUserInfo(user))
+ .ToArrayAsync();
+ }
+
+ public async Task<PutResult> 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);
+ }
+ }
+}