aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline.Tests/Helpers/TestUsers.cs4
-rw-r--r--Timeline.Tests/Helpers/UserInfoComparers.cs49
-rw-r--r--Timeline.Tests/JwtTokenUnitTest.cs2
-rw-r--r--Timeline.Tests/UserUnitTest.cs4
-rw-r--r--Timeline/Controllers/UserController.cs8
-rw-r--r--Timeline/Entities/Http/User.cs16
-rw-r--r--Timeline/Entities/UserInfo.cs74
-rw-r--r--Timeline/Entities/UserUtility.cs49
-rw-r--r--Timeline/Services/JwtService.cs39
-rw-r--r--Timeline/Services/UserService.cs39
10 files changed, 170 insertions, 114 deletions
diff --git a/Timeline.Tests/Helpers/TestUsers.cs b/Timeline.Tests/Helpers/TestUsers.cs
index 89ddf218..dd00e38d 100644
--- a/Timeline.Tests/Helpers/TestUsers.cs
+++ b/Timeline.Tests/Helpers/TestUsers.cs
@@ -28,8 +28,8 @@ namespace Timeline.Tests.Helpers
MockUsers = mockUsers;
- var mockUserInfos = mockUsers.Select(u => UserInfo.Create(u)).ToList();
- mockUserInfos.Sort(UserInfo.Comparer);
+ var mockUserInfos = mockUsers.Select(u => UserUtility.CreateUserInfo(u)).ToList();
+ mockUserInfos.Sort(UserInfoComparers.Comparer);
MockUserInfos = mockUserInfos;
}
diff --git a/Timeline.Tests/Helpers/UserInfoComparers.cs b/Timeline.Tests/Helpers/UserInfoComparers.cs
new file mode 100644
index 00000000..d88b6622
--- /dev/null
+++ b/Timeline.Tests/Helpers/UserInfoComparers.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Timeline.Entities;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class UserInfoComparers
+ {
+ public static IEqualityComparer<UserInfo> EqualityComparer { get; } = new EqualityComparerImpl();
+ public static IComparer<UserInfo> Comparer { get; } = Comparer<UserInfo>.Create(Compare);
+
+
+ private class EqualityComparerImpl : IEqualityComparer<UserInfo>
+ {
+ bool IEqualityComparer<UserInfo>.Equals(UserInfo x, UserInfo y)
+ {
+ return Compare(x, y) == 0;
+ }
+
+ int IEqualityComparer<UserInfo>.GetHashCode(UserInfo obj)
+ {
+ return obj.Username.GetHashCode() ^ obj.IsAdmin.GetHashCode();
+ }
+ }
+
+ public static int Compare(UserInfo left, UserInfo right)
+ {
+ if (left == null)
+ {
+ if (right == null)
+ return 0;
+ return -1;
+ }
+
+ if (right == null)
+ return 1;
+
+ var uc = string.Compare(left.Username, right.Username);
+ if (uc != 0)
+ return uc;
+
+ if (left.IsAdmin == right.IsAdmin)
+ return 0;
+
+ return left.IsAdmin ? -1 : 1;
+ }
+ }
+}
diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs
index 8a503bd7..a4e5432f 100644
--- a/Timeline.Tests/JwtTokenUnitTest.cs
+++ b/Timeline.Tests/JwtTokenUnitTest.cs
@@ -78,7 +78,7 @@ namespace Timeline.Tests
Assert.True(result.IsValid);
Assert.NotNull(result.UserInfo);
Assert.Equal(createTokenResult.UserInfo.Username, result.UserInfo.Username);
- Assert.Equal(createTokenResult.UserInfo.Roles, result.UserInfo.Roles);
+ Assert.Equal(createTokenResult.UserInfo.IsAdmin, result.UserInfo.IsAdmin);
}
}
}
diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs
index 7d8cc824..a4b4dace 100644
--- a/Timeline.Tests/UserUnitTest.cs
+++ b/Timeline.Tests/UserUnitTest.cs
@@ -28,8 +28,8 @@ namespace Timeline.Tests
var res1 = await client.GetAsync("users");
Assert.Equal(HttpStatusCode.OK, res1.StatusCode);
var users = JsonConvert.DeserializeObject<UserInfo[]>(await res1.Content.ReadAsStringAsync()).ToList();
- users.Sort(UserInfo.Comparer);
- Assert.Equal(TestMockUsers.MockUserInfos, users, UserInfo.EqualityComparer);
+ users.Sort(UserInfoComparers.Comparer);
+ Assert.Equal(TestMockUsers.MockUserInfos, users, UserInfoComparers.EqualityComparer);
}
}
}
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs
index a18e36e9..6f708e8a 100644
--- a/Timeline/Controllers/UserController.cs
+++ b/Timeline/Controllers/UserController.cs
@@ -37,9 +37,9 @@ namespace Timeline.Controllers
}
[HttpPut("user/{username}"), Authorize(Roles = "admin")]
- public async Task<IActionResult> Put([FromBody] UserModifyRequest request, [FromRoute] string username)
+ public async Task<IActionResult> Put([FromBody] UserPutRequest request, [FromRoute] string username)
{
- var result = await _userService.PutUser(username, request.Password, request.Roles);
+ var result = await _userService.PutUser(username, request.Password, request.IsAdmin);
switch (result)
{
case PutUserResult.Created:
@@ -52,9 +52,9 @@ namespace Timeline.Controllers
}
[HttpPatch("user/{username}"), Authorize(Roles = "admin")]
- public async Task<IActionResult> Patch([FromBody] UserModifyRequest request, [FromRoute] string username)
+ public async Task<IActionResult> Patch([FromBody] UserPatchRequest request, [FromRoute] string username)
{
- var result = await _userService.PatchUser(username, request.Password, request.Roles);
+ var result = await _userService.PatchUser(username, request.Password, request.IsAdmin);
switch (result)
{
case PatchUserResult.Success:
diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs
index 31cafaa3..db3d5071 100644
--- a/Timeline/Entities/Http/User.cs
+++ b/Timeline/Entities/Http/User.cs
@@ -1,9 +1,15 @@
namespace Timeline.Entities.Http
{
- public class UserModifyRequest
+ public class UserPutRequest
{
public string Password { get; set; }
- public string[] Roles { get; set; }
+ public bool IsAdmin { get; set; }
+ }
+
+ public class UserPatchRequest
+ {
+ public string Password { get; set; }
+ public bool? IsAdmin { get; set; }
}
public static class UserPutResponse
@@ -47,8 +53,8 @@
public const int ForbiddenCode = 1;
public const int NotExistsCode = 2;
- public static ReturnCodeMessageResponse Success {get;} = new ReturnCodeMessageResponse(SuccessCode, "Success to upload avatar.");
- public static ReturnCodeMessageResponse Forbidden {get;} = new ReturnCodeMessageResponse(ForbiddenCode, "You are not allowed to upload the user's avatar.");
- public static ReturnCodeMessageResponse NotExists {get;} = new ReturnCodeMessageResponse(NotExistsCode, "The username does not exists. If you are a user, try update your token.");
+ public static ReturnCodeMessageResponse Success { get; } = new ReturnCodeMessageResponse(SuccessCode, "Success to upload avatar.");
+ public static ReturnCodeMessageResponse Forbidden { get; } = new ReturnCodeMessageResponse(ForbiddenCode, "You are not allowed to upload the user's avatar.");
+ public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "The username does not exists. If you are a user, try update your token.");
}
}
diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs
index c9bcde5b..bb56df9d 100644
--- a/Timeline/Entities/UserInfo.cs
+++ b/Timeline/Entities/UserInfo.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Timeline.Models;
namespace Timeline.Entities
{
@@ -11,83 +10,18 @@ namespace Timeline.Entities
{
}
- public UserInfo(string username, params string[] roles)
+ public UserInfo(string username, bool isAdmin)
{
Username = username;
- Roles = roles;
+ IsAdmin = isAdmin;
}
- public static UserInfo Create(User user)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
- return Create(user.Name, user.RoleString);
- }
-
- public static UserInfo Create(string username, string roleString) => new UserInfo
- {
- Username = username,
- Roles = RolesFromString(roleString)
- };
-
public string Username { get; set; }
- public string[] Roles { get; set; }
-
- public static IEqualityComparer<UserInfo> EqualityComparer { get; } = new EqualityComparerImpl();
- public static IComparer<UserInfo> Comparer { get; } = Comparer<UserInfo>.Create(Compare);
-
- private static string[] RolesFromString(string roleString)
- {
- if (roleString == null)
- return null;
- return roleString.Split(',').Select(r => r.Trim()).ToArray();
- }
-
- private class EqualityComparerImpl : IEqualityComparer<UserInfo>
- {
- bool IEqualityComparer<UserInfo>.Equals(UserInfo x, UserInfo y)
- {
- return Compare(x, y) == 0;
- }
-
- int IEqualityComparer<UserInfo>.GetHashCode(UserInfo obj)
- {
- return obj.Username.GetHashCode() ^ NormalizeRoles(obj.Roles).GetHashCode();
- }
- }
-
- private static string NormalizeRoles(string[] rawRoles)
- {
- var roles = rawRoles.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()).ToList();
- roles.Sort();
- return string.Join(',', roles);
- }
-
- public static int Compare(UserInfo left, UserInfo right)
- {
- if (left == null)
- {
- if (right == null)
- return 0;
- return -1;
- }
-
- if (right == null)
- return 1;
-
- var uc = string.Compare(left.Username, right.Username);
- if (uc != 0)
- return uc;
-
- var leftRoles = NormalizeRoles(left.Roles);
- var rightRoles = NormalizeRoles(right.Roles);
-
- return string.Compare(leftRoles, rightRoles);
- }
+ public bool IsAdmin { get; set; }
public override string ToString()
{
- return $"Username: {Username} ; Roles: {Roles}";
+ return $"Username: {Username} ; IsAdmin: {IsAdmin}";
}
}
}
diff --git a/Timeline/Entities/UserUtility.cs b/Timeline/Entities/UserUtility.cs
new file mode 100644
index 00000000..9a272948
--- /dev/null
+++ b/Timeline/Entities/UserUtility.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Linq;
+using Timeline.Entities;
+using Timeline.Models;
+
+namespace Timeline.Entities
+{
+ public static class UserUtility
+ {
+ public const string UserRole = "user";
+ public const string AdminRole = "admin";
+
+ public static string[] UserRoleArray { get; } = new string[] { UserRole };
+ public static string[] AdminRoleArray { get; } = new string[] { UserRole, AdminRole };
+
+ public static string[] IsAdminToRoleArray(bool isAdmin)
+ {
+ return isAdmin ? AdminRoleArray : UserRoleArray;
+ }
+
+ public static bool RoleArrayToIsAdmin(string[] roles)
+ {
+ return roles.Contains(AdminRole);
+ }
+
+ public static string[] RoleStringToRoleArray(string roleString)
+ {
+ return roleString.Split(',').ToArray();
+ }
+
+ public static string RoleArrayToRoleString(string[] roles)
+ {
+ return string.Join(',', roles);
+ }
+
+ public static string IsAdminToRoleString(bool isAdmin)
+ {
+ return RoleArrayToRoleString(IsAdminToRoleArray(isAdmin));
+ }
+
+ public static UserInfo CreateUserInfo(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ return new UserInfo(user.Name, RoleArrayToIsAdmin(RoleStringToRoleArray(user.RoleString)));
+ }
+
+ }
+}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs
index bf470354..f5df59a5 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -11,24 +11,28 @@ using Timeline.Entities;
namespace Timeline.Services
{
+ public class TokenInfo
+ {
+ public string Name { get; set; }
+ public string[] Roles { get; set; }
+ }
+
public interface IJwtService
{
/// <summary>
/// Create a JWT token for a given user info.
/// </summary>
- /// <param name="userId">The user id contained in generate token.</param>
- /// <param name="username">The username contained in token.</param>
- /// <param name="roles">The roles contained in token.</param>
+ /// <param name="tokenInfo">The info to generate token.</param>
/// <returns>Return the generated token.</returns>
- string GenerateJwtToken(long userId, string username, string[] roles);
+ string GenerateJwtToken(TokenInfo tokenInfo);
/// <summary>
/// Verify a JWT token.
/// Return null is <paramref name="token"/> is null.
/// </summary>
/// <param name="token">The token string to verify.</param>
- /// <returns>Return null if <paramref name="token"/> is null or token is invalid. Return the saved user info otherwise.</returns>
- UserInfo VerifyJwtToken(string token);
+ /// <returns>Return null if <paramref name="token"/> is null or token is invalid. Return the saved info otherwise.</returns>
+ TokenInfo VerifyJwtToken(string token);
}
@@ -44,14 +48,20 @@ namespace Timeline.Services
_logger = logger;
}
- public string GenerateJwtToken(long id, string username, string[] roles)
+ public string GenerateJwtToken(TokenInfo tokenInfo)
{
+ if (tokenInfo == null)
+ throw new ArgumentNullException(nameof(tokenInfo));
+ if (tokenInfo.Name == null)
+ throw new ArgumentException("Name is null.", nameof(tokenInfo));
+ if (tokenInfo.Roles == null)
+ throw new ArgumentException("Roles is null.", nameof(tokenInfo));
+
var jwtConfig = _jwtConfig.CurrentValue;
var identity = new ClaimsIdentity();
- identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id.ToString()));
- identity.AddClaim(new Claim(identity.NameClaimType, username));
- identity.AddClaims(roles.Select(role => new Claim(identity.RoleClaimType, role)));
+ identity.AddClaim(new Claim(identity.NameClaimType, tokenInfo.Name));
+ identity.AddClaims(tokenInfo.Roles.Select(role => new Claim(identity.RoleClaimType, role)));
var tokenDescriptor = new SecurityTokenDescriptor()
{
@@ -71,7 +81,7 @@ namespace Timeline.Services
}
- public UserInfo VerifyJwtToken(string token)
+ public TokenInfo VerifyJwtToken(string token)
{
if (token == null)
return null;
@@ -90,8 +100,11 @@ namespace Timeline.Services
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
}, out SecurityToken validatedToken);
- return new UserInfo(principal.Identity.Name,
- principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray());
+ return new TokenInfo
+ {
+ Name = principal.Identity.Name,
+ Roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray()
+ };
}
catch (Exception e)
{
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 8ab3bc54..9fe9e08f 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
+using static Timeline.Entities.UserUtility;
namespace Timeline.Services
{
@@ -120,7 +121,7 @@ namespace Timeline.Services
/// <param name="roles">Array of roles of user.</param>
/// <returns>Return <see cref="PutUserResult.Created"/> if a new user is created.
/// Return <see cref="PutUserResult.Modified"/> if a existing user is modified.</returns>
- Task<PutUserResult> PutUser(string username, string password, string[] roles);
+ Task<PutUserResult> PutUser(string username, string password, bool isAdmin);
/// <summary>
/// Partially modify a use of given username.
@@ -130,7 +131,7 @@ namespace Timeline.Services
/// <param name="roles">New roles. If not modify, then null.</param>
/// <returns>Return <see cref="PatchUserResult.Success"/> if modification succeeds.
/// Return <see cref="PatchUserResult.NotExists"/> if the user of given username doesn't exist.</returns>
- Task<PatchUserResult> PatchUser(string username, string password, string[] roles);
+ Task<PatchUserResult> PatchUser(string username, string password, bool? isAdmin);
/// <summary>
/// Delete a user of given username.
@@ -203,12 +204,16 @@ namespace Timeline.Services
if (verifyResult)
{
- var userInfo = UserInfo.Create(user);
-
+ var roles = RoleStringToRoleArray(user.RoleString);
+ var token = _jwtService.GenerateJwtToken(new TokenInfo
+ {
+ Name = username,
+ Roles = roles
+ });
return new CreateTokenResult
{
- Token = _jwtService.GenerateJwtToken(user.Id, userInfo.Username, userInfo.Roles),
- UserInfo = userInfo
+ Token = token,
+ UserInfo = new UserInfo(username, RoleArrayToIsAdmin(roles))
};
}
else
@@ -220,33 +225,33 @@ namespace Timeline.Services
public async Task<UserInfo> VerifyToken(string token)
{
- var userInfo = _jwtService.VerifyJwtToken(token);
+ var tokenInfo = _jwtService.VerifyJwtToken(token);
- if (userInfo == null)
+ if (tokenInfo == null)
{
_logger.LogInformation($"Verify token falied. Reason: invalid token. Token: {token} .");
return null;
}
- return await Task.FromResult(userInfo);
+ return await Task.FromResult(new UserInfo(tokenInfo.Name, RoleArrayToIsAdmin(tokenInfo.Roles)));
}
public async Task<UserInfo> GetUser(string username)
{
return await _databaseContext.Users
.Where(user => user.Name == username)
- .Select(user => UserInfo.Create(user.Name, user.RoleString))
+ .Select(user => CreateUserInfo(user))
.SingleOrDefaultAsync();
}
public async Task<UserInfo[]> ListUsers()
{
return await _databaseContext.Users
- .Select(user => UserInfo.Create(user.Name, user.RoleString))
+ .Select(user => CreateUserInfo(user))
.ToArrayAsync();
}
- public async Task<PutUserResult> PutUser(string username, string password, string[] roles)
+ public async Task<PutUserResult> PutUser(string username, string password, bool isAdmin)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
@@ -256,20 +261,20 @@ namespace Timeline.Services
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = string.Join(',', roles)
+ RoleString = IsAdminToRoleString(isAdmin)
});
await _databaseContext.SaveChangesAsync();
return PutUserResult.Created;
}
user.EncryptedPassword = _passwordService.HashPassword(password);
- user.RoleString = string.Join(',', roles);
+ user.RoleString = IsAdminToRoleString(isAdmin);
await _databaseContext.SaveChangesAsync();
return PutUserResult.Modified;
}
- public async Task<PatchUserResult> PatchUser(string username, string password, string[] roles)
+ public async Task<PatchUserResult> PatchUser(string username, string password, bool? isAdmin)
{
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
@@ -284,10 +289,10 @@ namespace Timeline.Services
user.EncryptedPassword = _passwordService.HashPassword(password);
}
- if (roles != null)
+ if (isAdmin != null)
{
modified = true;
- user.RoleString = string.Join(',', roles);
+ user.RoleString = IsAdminToRoleString(isAdmin.Value);
}
if (modified)