From b86c8cf5130d21ac56e733640cecd08945d30e6d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Apr 2019 21:23:21 +0800 Subject: Add user management REST api. --- Timeline/Services/UserService.cs | 155 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 146 insertions(+), 9 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ad36c37b..caeb4efe 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -13,10 +13,40 @@ namespace Timeline.Services public UserInfo UserInfo { get; set; } } - public enum CreateUserResult + public enum PutUserResult { + /// + /// A new user is created. + /// + Created, + /// + /// A existing user is modified. + /// + Modified + } + + public enum PatchUserResult + { + /// + /// Succeed to modify user. + /// Success, - AlreadyExists + /// + /// A user of given username does not exist. + /// + NotExists + } + + public enum DeleteUserResult + { + /// + /// Succeed to delete user. + /// + Success, + /// + /// A user of given username does not exist. + /// + NotExists } public interface IUserService @@ -38,7 +68,51 @@ namespace Timeline.Services /// Return null if verification failed. The user info if verification succeeded. Task VerifyToken(string token); - Task CreateUser(string username, string password, string[] roles); + /// + /// Get the user info of given username. + /// + /// Username of the user. + /// The info of the user. Null if the user of given username does not exists. + Task GetUser(string username); + + /// + /// List all users. + /// + /// The user info of users. + Task ListUsers(); + + /// + /// Create or modify a user with given username. + /// Return if a new user is created. + /// Return if a existing user is modified. + /// + /// Username of user. + /// Password of user. + /// Array of roles of user. + /// Return if a new user is created. + /// Return if a existing user is modified. + Task PutUser(string username, string password, string[] roles); + + /// + /// Partially modify a use of given username. + /// + /// Username of the user to modify. + /// New password. If not modify, then null. + /// New roles. If not modify, then null. + /// Return if modification succeeds. + /// Return if the user of given username doesn't exist. + Task PatchUser(string username, string password, string[] roles); + + /// + /// Delete a user of given username. + /// Return if success to delete. + /// Return if the user of given username + /// does not exist. + /// + /// Username of thet user to delete. + /// if success to delete. + /// if the user doesn't exist. + Task DeleteUser(string username); } public class UserService : IUserService @@ -108,19 +182,82 @@ namespace Timeline.Services return new UserInfo(user); } - public async Task CreateUser(string username, string password, string[] roles) + public async Task GetUser(string username) { - var exists = (await _databaseContext.Users.Where(u => u.Name == username).ToListAsync()).Count != 0; + return await _databaseContext.Users + .Where(user => user.Name == username) + .Select(user => new UserInfo(user)).SingleOrDefaultAsync(); + } - if (exists) + public async Task ListUsers() + { + return await _databaseContext.Users.Select(user => new UserInfo(user)).ToArrayAsync(); + } + + public async Task PutUser(string username, string password, string[] roles) + { + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + + if (user == null) { - return CreateUserResult.AlreadyExists; + await _databaseContext.AddAsync(new User + { + Name = username, + EncryptedPassword = _passwordService.HashPassword(password), + RoleString = string.Join(',', roles) + }); + await _databaseContext.SaveChangesAsync(); + return PutUserResult.Created; } - await _databaseContext.Users.AddAsync(new User { Name = username, EncryptedPassword = _passwordService.HashPassword(password), RoleString = string.Join(',', roles) }); + user.EncryptedPassword = _passwordService.HashPassword(password); + user.RoleString = string.Join(',', roles); await _databaseContext.SaveChangesAsync(); - return CreateUserResult.Success; + return PutUserResult.Modified; + } + + public async Task PatchUser(string username, string password, string[] roles) + { + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + + if (user == null) + return PatchUserResult.NotExists; + + bool modified = false; + + if (password != null) + { + modified = true; + user.EncryptedPassword = _passwordService.HashPassword(password); + } + + if (roles != null) + { + modified = true; + user.RoleString = string.Join(',', roles); + } + + if (modified) + { + await _databaseContext.SaveChangesAsync(); + } + + return PatchUserResult.Success; + } + + public async Task DeleteUser(string username) + { + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + + if (user == null) + { + return DeleteUserResult.NotExists; + } + + _databaseContext.Users.Remove(user); + await _databaseContext.SaveChangesAsync(); + return DeleteUserResult.Success; } } } -- cgit v1.2.3 From edac531b7c0bfc40bc9b6f5c6f1abe71f71cd9e0 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 21 Apr 2019 22:39:42 +0800 Subject: Remove unnecessary columns in database query. --- Timeline/Entities/UserInfo.cs | 23 +++++++++++++++-------- Timeline/Services/UserService.cs | 18 +++++++++++------- Timeline/Startup.cs | 32 +++++++++++++++----------------- 3 files changed, 41 insertions(+), 32 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs index a1860552..c9bcde5b 100644 --- a/Timeline/Entities/UserInfo.cs +++ b/Timeline/Entities/UserInfo.cs @@ -17,25 +17,32 @@ namespace Timeline.Entities Roles = roles; } - public UserInfo(User user) + public static UserInfo Create(User user) { if (user == null) throw new ArgumentNullException(nameof(user)); - - Username = user.Name; - - if (user.RoleString == null) - Roles = null; - else - Roles = user.RoleString.Split(',').Select(r => r.Trim()).ToArray(); + 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 EqualityComparer { get; } = new EqualityComparerImpl(); public static IComparer Comparer { get; } = Comparer.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 { bool IEqualityComparer.Equals(UserInfo x, UserInfo y) diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index caeb4efe..34eeb1ad 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -132,8 +132,6 @@ namespace Timeline.Services public async Task CreateToken(string username, string password) { - var users = _databaseContext.Users.ToList(); - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) @@ -146,7 +144,7 @@ namespace Timeline.Services if (verifyResult) { - var userInfo = new UserInfo(user); + var userInfo = UserInfo.Create(user); return new CreateTokenResult { @@ -171,7 +169,10 @@ namespace Timeline.Services return null; } - var user = await _databaseContext.Users.Where(u => u.Id == userId.Value).SingleOrDefaultAsync(); + var user = await _databaseContext.Users + .Where(u => u.Id == userId.Value) + .Select(u => UserInfo.Create(u.Name, u.RoleString)) + .SingleOrDefaultAsync(); if (user == null) { @@ -179,19 +180,22 @@ namespace Timeline.Services return null; } - return new UserInfo(user); + return user; } public async Task GetUser(string username) { return await _databaseContext.Users .Where(user => user.Name == username) - .Select(user => new UserInfo(user)).SingleOrDefaultAsync(); + .Select(user => UserInfo.Create(user.Name, user.RoleString)) + .SingleOrDefaultAsync(); } public async Task ListUsers() { - return await _databaseContext.Users.Select(user => new UserInfo(user)).ToArrayAsync(); + return await _databaseContext.Users + .Select(user => UserInfo.Create(user.Name, user.RoleString)) + .ToArrayAsync(); } public async Task PutUser(string username, string password, string[] roles) diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 0c8d7052..285dfcfa 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -36,26 +37,16 @@ namespace Timeline options.InputFormatters.Add(new StringInputFormatter()); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - if (Environment.IsDevelopment()) - { - services.AddCors(options => - { - options.AddPolicy(corsPolicyName, builder => - { - builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials(); - }); - }); - } - else + services.AddCors(options => { - services.AddCors(options => + options.AddPolicy(corsPolicyName, builder => { - options.AddPolicy(corsPolicyName, builder => - { + if (Environment.IsProduction()) builder.WithOrigins("https://www.crupest.xyz", "https://crupest.xyz").AllowAnyMethod().AllowAnyHeader().AllowCredentials(); - }); + else + builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials(); }); - } + }); services.Configure(Configuration.GetSection(nameof(JwtConfig))); var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); @@ -80,7 +71,14 @@ namespace Timeline services.AddDbContext(options => { - options.UseMySql(databaseConfig.ConnectionString); + options.UseMySql(databaseConfig.ConnectionString) + .ConfigureWarnings(warnings => + { + if (Environment.IsProduction()) + warnings.Log(RelationalEventId.QueryClientEvaluationWarning); + else + warnings.Throw(RelationalEventId.QueryClientEvaluationWarning); + }); }); } -- cgit v1.2.3 From e347b4a4092a24ff7106ffd3aca67d6ca7decca8 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 21 Apr 2019 23:23:49 +0800 Subject: Allow ordinary user to patch his password. --- Timeline/Controllers/UserController.cs | 39 ++++++++++++++++++++++++++-------- Timeline/Entities/AdminUser.cs | 30 -------------------------- Timeline/Entities/Common.cs | 12 +++++++++++ Timeline/Entities/Token.cs | 26 +++++++++++++++++++++++ Timeline/Entities/User.cs | 30 ++++++++++++++------------ Timeline/Services/JwtService.cs | 22 +++++++++++-------- Timeline/Services/UserService.cs | 21 +++++------------- 7 files changed, 103 insertions(+), 77 deletions(-) delete mode 100644 Timeline/Entities/AdminUser.cs create mode 100644 Timeline/Entities/Common.cs create mode 100644 Timeline/Entities/Token.cs (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index ab7e1b99..d2708eeb 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -48,18 +48,39 @@ namespace Timeline.Controllers } } - [HttpPatch("user/{username}"), Authorize(Roles = "admin")] + [HttpPatch("user/{username}"), Authorize] public async Task Patch([FromBody] UserModifyRequest request, [FromRoute] string username) { - var result = await _userService.PatchUser(username, request.Password, request.Roles); - switch (result) + if (User.IsInRole("admin")) { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(); - default: - throw new Exception("Unreachable code."); + var result = await _userService.PatchUser(username, request.Password, request.Roles); + switch (result) + { + case PatchUserResult.Success: + return Ok(); + case PatchUserResult.NotExists: + return NotFound(); + default: + throw new Exception("Unreachable code."); + } + } + else + { + if (User.Identity.Name != username) + return StatusCode(403, new MessageResponse("Can't patch other user when you are not admin.")); + if (request.Roles != null) + return StatusCode(403, new MessageResponse("Can't patch roles when you are not admin.")); + + var result = await _userService.PatchUser(username, request.Password, null); + switch (result) + { + case PatchUserResult.Success: + return Ok(); + case PatchUserResult.NotExists: + return NotFound(new MessageResponse("This username no longer exists. Please update your token.")); + default: + throw new Exception("Unreachable code."); + } } } diff --git a/Timeline/Entities/AdminUser.cs b/Timeline/Entities/AdminUser.cs deleted file mode 100644 index eb126165..00000000 --- a/Timeline/Entities/AdminUser.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Timeline.Entities -{ - public class UserModifyRequest - { - public string Password { get; set; } - public string[] Roles { get; set; } - } - - public class UserPutResponse - { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; - - public static UserPutResponse Created { get; } = new UserPutResponse { ReturnCode = CreatedCode }; - public static UserPutResponse Modified { get; } = new UserPutResponse { ReturnCode = ModifiedCode }; - - public int ReturnCode { get; set; } - } - - public class UserDeleteResponse - { - public const int SuccessCode = 0; - public const int NotExistsCode = 1; - - public static UserDeleteResponse Success { get; } = new UserDeleteResponse { ReturnCode = SuccessCode }; - public static UserDeleteResponse NotExists { get; } = new UserDeleteResponse { ReturnCode = NotExistsCode }; - - public int ReturnCode { get; set; } - } -} diff --git a/Timeline/Entities/Common.cs b/Timeline/Entities/Common.cs new file mode 100644 index 00000000..235a2a20 --- /dev/null +++ b/Timeline/Entities/Common.cs @@ -0,0 +1,12 @@ +namespace Timeline.Entities +{ + public class MessageResponse + { + public MessageResponse(string message) + { + Message = message; + } + + public string Message { get; set; } + } +} diff --git a/Timeline/Entities/Token.cs b/Timeline/Entities/Token.cs new file mode 100644 index 00000000..1b5a469d --- /dev/null +++ b/Timeline/Entities/Token.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities +{ + public class CreateTokenRequest + { + public string Username { get; set; } + public string Password { get; set; } + } + + public class CreateTokenResponse + { + public bool Success { get; set; } + public string Token { get; set; } + public UserInfo UserInfo { get; set; } + } + + public class VerifyTokenRequest + { + public string Token { get; set; } + } + + public class VerifyTokenResponse + { + public bool IsValid { get; set; } + public UserInfo UserInfo { get; set; } + } +} diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs index 1b5a469d..eb126165 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/User.cs @@ -1,26 +1,30 @@ namespace Timeline.Entities { - public class CreateTokenRequest + public class UserModifyRequest { - public string Username { get; set; } public string Password { get; set; } + public string[] Roles { get; set; } } - public class CreateTokenResponse + public class UserPutResponse { - public bool Success { get; set; } - public string Token { get; set; } - public UserInfo UserInfo { get; set; } - } + public const int CreatedCode = 0; + public const int ModifiedCode = 1; - public class VerifyTokenRequest - { - public string Token { get; set; } + public static UserPutResponse Created { get; } = new UserPutResponse { ReturnCode = CreatedCode }; + public static UserPutResponse Modified { get; } = new UserPutResponse { ReturnCode = ModifiedCode }; + + public int ReturnCode { get; set; } } - public class VerifyTokenResponse + public class UserDeleteResponse { - public bool IsValid { get; set; } - public UserInfo UserInfo { get; set; } + public const int SuccessCode = 0; + public const int NotExistsCode = 1; + + public static UserDeleteResponse Success { get; } = new UserDeleteResponse { ReturnCode = SuccessCode }; + public static UserDeleteResponse NotExists { get; } = new UserDeleteResponse { ReturnCode = NotExistsCode }; + + public int ReturnCode { get; set; } } } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index 91e7f879..bf470354 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -7,25 +7,28 @@ using System.Linq; using System.Security.Claims; using System.Text; using Timeline.Configs; +using Timeline.Entities; namespace Timeline.Services { public interface IJwtService { /// - /// Create a JWT token for a given user id. + /// Create a JWT token for a given user info. /// - /// The user id used to generate token. + /// The user id contained in generate token. + /// The username contained in token. + /// The roles contained in token. /// Return the generated token. - string GenerateJwtToken(long userId, string[] roles); + string GenerateJwtToken(long userId, string username, string[] roles); /// /// Verify a JWT token. /// Return null is is null. /// /// The token string to verify. - /// Return null if is null or token is invalid. Return the saved user id otherwise. - long? VerifyJwtToken(string token); + /// Return null if is null or token is invalid. Return the saved user info otherwise. + UserInfo VerifyJwtToken(string token); } @@ -41,12 +44,13 @@ namespace Timeline.Services _logger = logger; } - public string GenerateJwtToken(long id, string[] roles) + public string GenerateJwtToken(long id, string username, string[] roles) { 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))); var tokenDescriptor = new SecurityTokenDescriptor() @@ -67,13 +71,12 @@ namespace Timeline.Services } - public long? VerifyJwtToken(string token) + public UserInfo VerifyJwtToken(string token) { if (token == null) return null; var config = _jwtConfig.CurrentValue; - try { var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters @@ -87,7 +90,8 @@ namespace Timeline.Services IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)) }, out SecurityToken validatedToken); - return long.Parse(principal.FindAll(ClaimTypes.NameIdentifier).Single().Value); + return new UserInfo(principal.Identity.Name, + 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 34eeb1ad..a0d358dd 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -101,7 +101,7 @@ namespace Timeline.Services /// New roles. If not modify, then null. /// Return if modification succeeds. /// Return if the user of given username doesn't exist. - Task PatchUser(string username, string password, string[] roles); + Task PatchUser(string username, string password, string[] roles); /// /// Delete a user of given username. @@ -148,7 +148,7 @@ namespace Timeline.Services return new CreateTokenResult { - Token = _jwtService.GenerateJwtToken(user.Id, userInfo.Roles), + Token = _jwtService.GenerateJwtToken(user.Id, userInfo.Username, userInfo.Roles), UserInfo = userInfo }; } @@ -161,26 +161,15 @@ namespace Timeline.Services public async Task VerifyToken(string token) { - var userId = _jwtService.VerifyJwtToken(token); + var userInfo = _jwtService.VerifyJwtToken(token); - if (userId == null) + if (userInfo == null) { _logger.LogInformation($"Verify token falied. Reason: invalid token. Token: {token} ."); return null; } - var user = await _databaseContext.Users - .Where(u => u.Id == userId.Value) - .Select(u => UserInfo.Create(u.Name, u.RoleString)) - .SingleOrDefaultAsync(); - - if (user == null) - { - _logger.LogInformation($"Verify token falied. Reason: invalid user id. UserId: {userId} Token: {token} ."); - return null; - } - - return user; + return await Task.FromResult(userInfo); } public async Task GetUser(string username) -- cgit v1.2.3 From 407f97db0be86aa071802b67bfdeadc7703528c9 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 22 Apr 2019 14:45:52 +0800 Subject: Move http models in to a new namespace. Revert last commit. --- .../Authentication/AuthenticationExtensions.cs | 2 +- Timeline.Tests/JwtTokenUnitTest.cs | 2 +- Timeline/Controllers/TokenController.cs | 2 +- Timeline/Controllers/UserController.cs | 46 ++++++---------------- Timeline/Entities/Common.cs | 12 ------ Timeline/Entities/Http/Common.cs | 29 ++++++++++++++ Timeline/Entities/Http/Token.cs | 26 ++++++++++++ Timeline/Entities/Http/User.cs | 26 ++++++++++++ Timeline/Entities/Token.cs | 26 ------------ Timeline/Entities/User.cs | 30 -------------- Timeline/Services/UserService.cs | 10 ++--- 11 files changed, 102 insertions(+), 109 deletions(-) delete mode 100644 Timeline/Entities/Common.cs create mode 100644 Timeline/Entities/Http/Common.cs create mode 100644 Timeline/Entities/Http/Token.cs create mode 100644 Timeline/Entities/Http/User.cs delete mode 100644 Timeline/Entities/Token.cs delete mode 100644 Timeline/Entities/User.cs (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index 40191009..cda9fe99 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -4,7 +4,7 @@ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Entities; +using Timeline.Entities.Http; using Xunit; namespace Timeline.Tests.Helpers.Authentication diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index 39ffc928..8a503bd7 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using System.Net; using System.Net.Http; -using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Xunit; diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 463fb83c..0be5fb2f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Threading.Tasks; -using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Services; namespace Timeline.Controllers diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index d2708eeb..59c7a48c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Entities.Http; using Timeline.Services; namespace Timeline.Controllers @@ -48,50 +49,29 @@ namespace Timeline.Controllers } } - [HttpPatch("user/{username}"), Authorize] + [HttpPatch("user/{username}"), Authorize(Roles = "admin")] public async Task Patch([FromBody] UserModifyRequest request, [FromRoute] string username) { - if (User.IsInRole("admin")) - { - var result = await _userService.PatchUser(username, request.Password, request.Roles); - switch (result) - { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(); - default: - throw new Exception("Unreachable code."); - } - } - else + var result = await _userService.PatchUser(username, request.Password, request.Roles); + switch (result) { - if (User.Identity.Name != username) - return StatusCode(403, new MessageResponse("Can't patch other user when you are not admin.")); - if (request.Roles != null) - return StatusCode(403, new MessageResponse("Can't patch roles when you are not admin.")); - - var result = await _userService.PatchUser(username, request.Password, null); - switch (result) - { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(new MessageResponse("This username no longer exists. Please update your token.")); - default: - throw new Exception("Unreachable code."); - } + case PatchUserResult.Success: + return Ok(); + case PatchUserResult.NotExists: + return NotFound(); + default: + throw new Exception("Unreachable code."); } } [HttpDelete("user/{username}"), Authorize(Roles = "admin")] - public async Task> Delete([FromRoute] string username) + public async Task Delete([FromRoute] string username) { var result = await _userService.DeleteUser(username); switch (result) { - case DeleteUserResult.Success: - return Ok(UserDeleteResponse.Success); + case DeleteUserResult.Deleted: + return Ok(UserDeleteResponse.Deleted); case DeleteUserResult.NotExists: return Ok(UserDeleteResponse.NotExists); default: diff --git a/Timeline/Entities/Common.cs b/Timeline/Entities/Common.cs deleted file mode 100644 index 235a2a20..00000000 --- a/Timeline/Entities/Common.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Timeline.Entities -{ - public class MessageResponse - { - public MessageResponse(string message) - { - Message = message; - } - - public string Message { get; set; } - } -} diff --git a/Timeline/Entities/Http/Common.cs b/Timeline/Entities/Http/Common.cs new file mode 100644 index 00000000..9575e6fa --- /dev/null +++ b/Timeline/Entities/Http/Common.cs @@ -0,0 +1,29 @@ +namespace Timeline.Entities.Http +{ + public class ReturnCodeMessageResponse + { + public ReturnCodeMessageResponse() + { + + } + + public ReturnCodeMessageResponse(int code) + { + ReturnCode = code; + } + + public ReturnCodeMessageResponse(string message) + { + Message = message; + } + + public ReturnCodeMessageResponse(int code, string message) + { + ReturnCode = code; + Message = message; + } + + public int? ReturnCode { get; set; } = null; + public string Message { get; set; } = null; + } +} diff --git a/Timeline/Entities/Http/Token.cs b/Timeline/Entities/Http/Token.cs new file mode 100644 index 00000000..45ee0fc5 --- /dev/null +++ b/Timeline/Entities/Http/Token.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities.Http +{ + public class CreateTokenRequest + { + public string Username { get; set; } + public string Password { get; set; } + } + + public class CreateTokenResponse + { + public bool Success { get; set; } + public string Token { get; set; } + public UserInfo UserInfo { get; set; } + } + + public class VerifyTokenRequest + { + public string Token { get; set; } + } + + public class VerifyTokenResponse + { + public bool IsValid { get; set; } + public UserInfo UserInfo { get; set; } + } +} diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs new file mode 100644 index 00000000..24952ac7 --- /dev/null +++ b/Timeline/Entities/Http/User.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities.Http +{ + public class UserModifyRequest + { + public string Password { get; set; } + public string[] Roles { get; set; } + } + + public static class UserPutResponse + { + public const int CreatedCode = 0; + public const int ModifiedCode = 1; + + public static ReturnCodeMessageResponse Created { get; } = new ReturnCodeMessageResponse(CreatedCode, "A new user is created."); + public static ReturnCodeMessageResponse Modified { get; } = new ReturnCodeMessageResponse(ModifiedCode, "A existing user is modified."); + } + + public static class UserDeleteResponse + { + public const int DeletedCode = 0; + public const int NotExistsCode = 1; + + public static ReturnCodeMessageResponse Deleted { get; } = new ReturnCodeMessageResponse(DeletedCode, "A existing user is deleted."); + public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "User with given name does not exists."); + } +} diff --git a/Timeline/Entities/Token.cs b/Timeline/Entities/Token.cs deleted file mode 100644 index 1b5a469d..00000000 --- a/Timeline/Entities/Token.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Timeline.Entities -{ - public class CreateTokenRequest - { - public string Username { get; set; } - public string Password { get; set; } - } - - public class CreateTokenResponse - { - public bool Success { get; set; } - public string Token { get; set; } - public UserInfo UserInfo { get; set; } - } - - public class VerifyTokenRequest - { - public string Token { get; set; } - } - - public class VerifyTokenResponse - { - public bool IsValid { get; set; } - public UserInfo UserInfo { get; set; } - } -} diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs deleted file mode 100644 index eb126165..00000000 --- a/Timeline/Entities/User.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Timeline.Entities -{ - public class UserModifyRequest - { - public string Password { get; set; } - public string[] Roles { get; set; } - } - - public class UserPutResponse - { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; - - public static UserPutResponse Created { get; } = new UserPutResponse { ReturnCode = CreatedCode }; - public static UserPutResponse Modified { get; } = new UserPutResponse { ReturnCode = ModifiedCode }; - - public int ReturnCode { get; set; } - } - - public class UserDeleteResponse - { - public const int SuccessCode = 0; - public const int NotExistsCode = 1; - - public static UserDeleteResponse Success { get; } = new UserDeleteResponse { ReturnCode = SuccessCode }; - public static UserDeleteResponse NotExists { get; } = new UserDeleteResponse { ReturnCode = NotExistsCode }; - - public int ReturnCode { get; set; } - } -} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index a0d358dd..8615d0c5 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -40,9 +40,9 @@ namespace Timeline.Services public enum DeleteUserResult { /// - /// Succeed to delete user. + /// A existing user is deleted. /// - Success, + Deleted, /// /// A user of given username does not exist. /// @@ -105,12 +105,12 @@ namespace Timeline.Services /// /// Delete a user of given username. - /// Return if success to delete. + /// Return if the user is deleted. /// Return if the user of given username /// does not exist. /// /// Username of thet user to delete. - /// if success to delete. + /// if the user is deleted. /// if the user doesn't exist. Task DeleteUser(string username); } @@ -250,7 +250,7 @@ namespace Timeline.Services _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); - return DeleteUserResult.Success; + return DeleteUserResult.Deleted; } } } -- cgit v1.2.3 From 80d686a8b875cce854e4291cbe4a53e7a03e3eff Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 22 Apr 2019 15:47:52 +0800 Subject: Add change password api. --- Timeline/Controllers/UserController.cs | 17 ++++++++++++++ Timeline/Entities/Http/User.cs | 17 ++++++++++++++ Timeline/Services/UserService.cs | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 59c7a48c..552bfb2f 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -78,5 +78,22 @@ namespace Timeline.Controllers throw new Exception("Uncreachable code."); } } + + [HttpPost("userop/changepassword"), Authorize] + public async Task ChangePassword([FromBody] ChangePasswordRequest request) + { + var result = await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword); + switch (result) + { + case ChangePasswordResult.Success: + return Ok(ChangePasswordResponse.Success); + case ChangePasswordResult.BadOldPassword: + return Ok(ChangePasswordResponse.BadOldPassword); + case ChangePasswordResult.NotExists: + return Ok(ChangePasswordResponse.NotExists); + default: + throw new Exception("Uncreachable code."); + } + } } } diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs index 24952ac7..d42ca088 100644 --- a/Timeline/Entities/Http/User.cs +++ b/Timeline/Entities/Http/User.cs @@ -23,4 +23,21 @@ public static ReturnCodeMessageResponse Deleted { get; } = new ReturnCodeMessageResponse(DeletedCode, "A existing user is deleted."); public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "User with given name does not exists."); } + + public class ChangePasswordRequest + { + public string OldPassword { get; set; } + public string NewPassword { get; set; } + } + + public static class ChangePasswordResponse + { + public const int SuccessCode = 0; + public const int BadOldPasswordCode = 1; + public const int NotExistsCode = 2; + + public static ReturnCodeMessageResponse Success { get; } = new ReturnCodeMessageResponse(SuccessCode, "Success to change password."); + public static ReturnCodeMessageResponse BadOldPassword { get; } = new ReturnCodeMessageResponse(BadOldPasswordCode, "Old password is wrong."); + public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "Username does not exists, please update token."); + } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 8615d0c5..75ad3331 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -49,6 +49,22 @@ namespace Timeline.Services NotExists } + public enum ChangePasswordResult + { + /// + /// Success to change password. + /// + Success, + /// + /// The user does not exists. + /// + NotExists, + /// + /// Old password is wrong. + /// + BadOldPassword + } + public interface IUserService { /// @@ -113,6 +129,17 @@ namespace Timeline.Services /// if the user is deleted. /// if the user doesn't exist. Task DeleteUser(string username); + + /// + /// Try to change a user's password with old password. + /// + /// The name of user to change password of. + /// The user's old password. + /// The user's new password. + /// if success. + /// if user does not exist. + /// if old password is wrong. + Task ChangePassword(string username, string oldPassword, string newPassword); } public class UserService : IUserService @@ -252,5 +279,20 @@ namespace Timeline.Services await _databaseContext.SaveChangesAsync(); return DeleteUserResult.Deleted; } + + public async Task ChangePassword(string username, string oldPassword, string newPassword) + { + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (user == null) + return ChangePasswordResult.NotExists; + + var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); + if (!verifyResult) + return ChangePasswordResult.BadOldPassword; + + user.EncryptedPassword = _passwordService.HashPassword(newPassword); + await _databaseContext.SaveChangesAsync(); + return ChangePasswordResult.Success; + } } } -- cgit v1.2.3 From 4249a52c856dcb4a33e8a381076542f563e89ac2 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 22 Apr 2019 17:15:06 +0800 Subject: Fix a bug in cos service. Add avatar api. --- Timeline/Controllers/UserController.cs | 8 ++++++++ Timeline/Services/TencentCloudCosService.cs | 5 +++++ Timeline/Services/UserService.cs | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 552bfb2f..1231befb 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -79,6 +79,14 @@ namespace Timeline.Controllers } } + [HttpGet("user/{username}/avatar"), Authorize] + public async Task GetAvatar([FromRoute] string username) + { + // TODO: test user existence. + var url = await _userService.GetAvatarUrl(username); + return Redirect(url); + } + [HttpPost("userop/changepassword"), Authorize] public async Task ChangePassword([FromBody] ChangePasswordRequest request) { diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index f1f52ec5..bc812e57 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -66,6 +66,11 @@ namespace Timeline.Services } if (serverException != null) { + if (serverException.statusCode == 404) + { + t.SetResult(false); + return; + } _logger.LogError(serverException, "An server error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); t.SetException(serverException); return; diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 75ad3331..a444d434 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -140,6 +140,8 @@ namespace Timeline.Services /// if user does not exist. /// if old password is wrong. Task ChangePassword(string username, string oldPassword, string newPassword); + + Task GetAvatarUrl(string username); } public class UserService : IUserService @@ -148,13 +150,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; + private readonly ITencentCloudCosService _cosService; - public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, ITencentCloudCosService cosService) { _logger = logger; _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; + _cosService = cosService; } public async Task CreateToken(string username, string password) @@ -294,5 +298,14 @@ namespace Timeline.Services await _databaseContext.SaveChangesAsync(); return ChangePasswordResult.Success; } + + public async Task GetAvatarUrl(string username) + { + var exists = await _cosService.Exists("avatar", username); + if (exists) + return _cosService.GetObjectUrl("avatar", username); + else + return _cosService.GetObjectUrl("avatar", "__default"); + } } } -- cgit v1.2.3 From 797b1da15c76f6598dcc48f675c1b82cb27a17ed Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 23 Apr 2019 00:14:35 +0800 Subject: Remove qcloud cs sdk. I will write one by myself. Develop signature algorithm. --- Timeline.Tests/QCloudCosServiceUnitTest.cs | 41 +++++++ Timeline/Configs/QCloudCosConfig.cs | 10 ++ Timeline/Configs/TencentCosConfig.cs | 13 --- Timeline/Services/TencentCloudCosService.cs | 174 +++++++++++++++++----------- Timeline/Services/UserService.cs | 6 +- Timeline/Startup.cs | 4 +- Timeline/Timeline.csproj | 1 - 7 files changed, 165 insertions(+), 84 deletions(-) create mode 100644 Timeline.Tests/QCloudCosServiceUnitTest.cs create mode 100644 Timeline/Configs/QCloudCosConfig.cs delete mode 100644 Timeline/Configs/TencentCosConfig.cs (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs new file mode 100644 index 00000000..c02f70be --- /dev/null +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Timeline.Services; +using Xunit; + +namespace Timeline.Tests +{ + public class QCloudCosServiceUnitTest + { + [Fact] + public void GenerateSignatureTest() + { + var credential = new QCloudCosService.QCloudCredentials + { + SecretId = "AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q", + SecretKey = "BQYIM75p8x0iWVFSIgqEKwFprpRSVHlz" + }; + + var request = new QCloudCosService.RequestInfo + { + Method = "put", + Uri = "/exampleobject", + Parameters = new Dictionary(), + Headers = new Dictionary + { + ["Host"] = "examplebucket-1250000000.cos.ap-beijing.myqcloud.com", + ["x-cos-storage-class"] = "standard", + ["x-cos-content-sha1"] = "b502c3a1f48c8609ae212cdfb639dee39673f5e" + } + }; + + var signValidTime = new QCloudCosService.TimeDuration + { + Start = DateTimeOffset.FromUnixTimeSeconds(1417773892), + End = DateTimeOffset.FromUnixTimeSeconds(1417853898) + }; + + Assert.Equal("q-sign-algorithm=sha1&q-ak=AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q&q-sign-time=1417773892;1417853898&q-key-time=1417773892;1417853898&q-header-list=host;x-cos-content-sha1;x-cos-storage-class&q-url-param-list=&q-signature=0ab12f43e74cbe148d705cd9fae8adc9a6d39cc1", QCloudCosService.GenerateSign(credential, request, signValidTime)); + } + } +} diff --git a/Timeline/Configs/QCloudCosConfig.cs b/Timeline/Configs/QCloudCosConfig.cs new file mode 100644 index 00000000..6d10436c --- /dev/null +++ b/Timeline/Configs/QCloudCosConfig.cs @@ -0,0 +1,10 @@ +namespace Timeline.Configs +{ + public class QCloudCosConfig + { + public string AppId { get; set; } + public string Region { get; set; } + public string SecretId { get; set; } + public string SecretKey { get; set; } + } +} diff --git a/Timeline/Configs/TencentCosConfig.cs b/Timeline/Configs/TencentCosConfig.cs deleted file mode 100644 index c41669f1..00000000 --- a/Timeline/Configs/TencentCosConfig.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -namespace Timeline.Configs -{ - public class TencentCosConfig - { - public string AppId { get; set; } - public string Region { get; set; } - public string SecretId { get; set; } - public string SecretKey { get; set; } - } -} diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index 9ab9d54d..1bfcf745 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -1,99 +1,143 @@ -using COSXML; -using COSXML.Auth; -using COSXML.CosException; -using COSXML.Model; -using COSXML.Model.Object; -using COSXML.Model.Tag; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using Timeline.Configs; namespace Timeline.Services { - public interface ITencentCloudCosService + public interface IQCloudCosService { - Task Exists(string bucket, string key); + Task ObjectExists(string bucket, string key); string GetObjectUrl(string bucket, string key); } - public class TencentCloudCosService : ITencentCloudCosService + public class QCloudCosService : IQCloudCosService { - private readonly TencentCosConfig _config; - private readonly CosXmlServer _server; - private readonly ILogger _logger; + private readonly QCloudCosConfig _config; + private readonly ILogger _logger; - public TencentCloudCosService(IOptions config, ILogger logger) + public QCloudCosService(IOptions config, ILogger logger) { _config = config.Value; _logger = logger; + } - var cosConfig = new CosXmlConfig.Builder() - .IsHttps(true) - .SetAppid(config.Value.AppId) - .SetRegion(config.Value.Region) - .SetDebugLog(true) - .Build(); + public class QCloudCredentials + { + public string SecretId { get; set; } + public string SecretKey { get; set; } + } - var credentialProvider = new DefaultQCloudCredentialProvider(config.Value.SecretId, config.Value.SecretKey, 3600); + public class RequestInfo + { + public string Method { get; set; } + public string Uri { get; set; } + public IEnumerable> Parameters { get; set; } + public IEnumerable> Headers { get; set; } + } - _server = new CosXmlServer(cosConfig, credentialProvider); + public class TimeDuration + { + public DateTimeOffset Start { get; set; } + public DateTimeOffset End { get; set; } } - public Task Exists(string bucket, string key) + public static string GenerateSign(QCloudCredentials credentials, RequestInfo request, TimeDuration signValidTime) { - bucket = bucket + "-" + _config.AppId; + Debug.Assert(credentials != null); + Debug.Assert(credentials.SecretId != null); + Debug.Assert(credentials.SecretKey != null); + Debug.Assert(request != null); + Debug.Assert(request.Method != null); + Debug.Assert(request.Uri != null); + Debug.Assert(request.Parameters != null); + Debug.Assert(request.Headers != null); + Debug.Assert(signValidTime != null); + Debug.Assert(signValidTime.Start < signValidTime.End, "Start must be before End in sign valid time."); + + List<(string key, string value)> Transform(IEnumerable> raw) + { + var sorted= raw.Select(p => (key: p.Key.ToLower(), value: WebUtility.UrlEncode(p.Value))).ToList(); + sorted.Sort((left, right) => string.CompareOrdinal(left.key, right.key)); + return sorted; + } + + var transformedParameters = Transform(request.Parameters); + var transformedHeaders = Transform(request.Headers); + + List<(string, string)> result = new List<(string, string)>(); + + const string signAlgorithm = "sha1"; + result.Add(("q-sign-algorithm", signAlgorithm)); + + result.Add(("q-ak", credentials.SecretId)); + + var signTime = $"{signValidTime.Start.ToUnixTimeSeconds().ToString()};{signValidTime.End.ToUnixTimeSeconds().ToString()}"; + var keyTime = signTime; + result.Add(("q-sign-time", signTime)); + result.Add(("q-key-time", keyTime)); - var request = new HeadObjectRequest(bucket, key); + result.Add(("q-header-list", string.Join(';', transformedHeaders.Select(h => h.key)))); + result.Add(("q-url-param-list", string.Join(';', transformedParameters.Select(p => p.key)))); - var t = new TaskCompletionSource(); + HMACSHA1 hmac = new HMACSHA1(); - _server.HeadObject(request, delegate (CosResult result) + string ByteArrayToString(byte[] bytes) { - if (result.httpCode >= 200 && result.httpCode < 300) - t.TrySetResult(true); - else - t.TrySetResult(false); - }, - delegate (CosClientException clientException, CosServerException serverException) + return BitConverter.ToString(bytes).Replace("-", "").ToLower(); + } + + hmac.Key = Encoding.UTF8.GetBytes(credentials.SecretKey); + var signKey = ByteArrayToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(keyTime))); + + string Join(IEnumerable<(string key, string value)> raw) { - if (clientException != null) - { - _logger.LogError(clientException, "An client error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(clientException); - return; - } - if (serverException != null) - { - if (serverException.statusCode == 404) - { - t.TrySetResult(false); - return; - } - _logger.LogError(serverException, "An server error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(serverException); - return; - } - _logger.LogError("An unknown error occured when test cos object existence. Bucket : {} . Key : {} .", bucket, key); - t.TrySetException(new Exception("Unknown exception when test cos object existence.")); - }); - - return t.Task; + return string.Join('&', raw.Select(p => string.Concat(p.key, "=", p.value))); + } + + var httpString = new StringBuilder() + .Append(request.Method).Append('\n') + .Append(request.Uri).Append('\n') + .Append(Join(transformedParameters)).Append('\n') + .Append(Join(transformedHeaders)).Append('\n') + .ToString(); + + string Sha1(string data) + { + var sha1 = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(data)); + return ByteArrayToString(sha1); + } + + var stringToSign = new StringBuilder() + .Append(signAlgorithm).Append('\n') + .Append(signTime).Append('\n') + .Append(Sha1(httpString)).Append('\n') + .ToString(); + + hmac.Key = Encoding.UTF8.GetBytes(signKey); + var signature = ByteArrayToString(hmac.ComputeHash( + Encoding.UTF8.GetBytes(stringToSign))); + + result.Add(("q-signature", signature)); + + return Join(result); + } + + public Task ObjectExists(string bucket, string key) + { + throw new NotImplementedException(); } public string GetObjectUrl(string bucket, string key) { - return _server.GenerateSignURL(new PreSignatureStruct() - { - appid = _config.AppId, - region = _config.Region, - bucket = bucket + "-" + _config.AppId, - key = key, - httpMethod = "GET", - isHttps = true, - signDurationSecond = 300 - }); + throw new NotImplementedException(); } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index a444d434..d1555660 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -150,9 +150,9 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; - private readonly ITencentCloudCosService _cosService; + private readonly IQCloudCosService _cosService; - public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, ITencentCloudCosService cosService) + public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService) { _logger = logger; _databaseContext = databaseContext; @@ -301,7 +301,7 @@ namespace Timeline.Services public async Task GetAvatarUrl(string username) { - var exists = await _cosService.Exists("avatar", username); + var exists = await _cosService.ObjectExists("avatar", username); if (exists) return _cosService.GetObjectUrl("avatar", username); else diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 6491554a..12d60843 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -81,8 +81,8 @@ namespace Timeline }); }); - services.Configure(Configuration.GetSection(nameof(TencentCosConfig))); - services.AddSingleton(); + services.Configure(Configuration.GetSection(nameof(QCloudCosConfig))); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index c9454ec9..93513bd3 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -12,6 +12,5 @@ - -- cgit v1.2.3 From 86e2384b8a186234a76611b7cbd505ad4f7dbd63 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 25 Apr 2019 18:53:05 +0800 Subject: Implement generate object get url in cos. --- Timeline.Tests/QCloudCosServiceUnitTest.cs | 45 +++++++++++++++++++--- Timeline/Services/TencentCloudCosService.cs | 58 +++++++++++++++++++++++++---- Timeline/Services/UserService.cs | 6 +-- 3 files changed, 93 insertions(+), 16 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs index b99352b9..b0e6a868 100644 --- a/Timeline.Tests/QCloudCosServiceUnitTest.cs +++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using Timeline.Services; using Timeline.Tests.Helpers; @@ -17,6 +18,19 @@ namespace Timeline.Tests public QCloudCosServiceUnitTest(WebApplicationFactory factory, ITestOutputHelper outputHelper) { _factory = factory.WithTestConfig(outputHelper); + _factory.CreateDefaultClient().Dispose(); // Ensure test server is created. + } + + [Fact] + public void ValidateBucketNameTest() + { + Assert.True(QCloudCosService.ValidateBucketName("hello")); + Assert.True(QCloudCosService.ValidateBucketName("hello0123")); + Assert.True(QCloudCosService.ValidateBucketName("hello0123-hello")); + Assert.False(QCloudCosService.ValidateBucketName("-hello")); + Assert.False(QCloudCosService.ValidateBucketName("hello-")); + Assert.False(QCloudCosService.ValidateBucketName("helloU")); + Assert.False(QCloudCosService.ValidateBucketName("hello!")); } [Fact] @@ -50,21 +64,40 @@ namespace Timeline.Tests Assert.Equal("q-sign-algorithm=sha1&q-ak=AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q&q-sign-time=1417773892;1417853898&q-key-time=1417773892;1417853898&q-header-list=host;x-cos-content-sha1;x-cos-storage-class&q-url-param-list=&q-signature=0ab12f43e74cbe148d705cd9fae8adc9a6d39cc1", QCloudCosService.GenerateSign(credential, request, signValidTime)); } - /* +/* +// Tests in this part need secret configs in cos. +#region SecretTests [Fact] public async Task ObjectExistsTest() { - _factory.CreateDefaultClient().Dispose(); + using (var serviceScope = _factory.Server.Host.Services.CreateScope()) + { + var services = serviceScope.ServiceProvider; + var service = services.GetRequiredService(); + Assert.True(await service.IsObjectExists("avatar", "__default")); + Assert.False(await service.IsObjectExists("avatar", "haha")); + Assert.False(await service.IsObjectExists("haha", "haha")); + } + } + // Although this test does not pass on my archlunux system. But the GenerateObjectGetUrl actually works well. + // And I don't know why. + [Fact] + public async Task GenerateObjectGetUrlTest() + { using (var serviceScope = _factory.Server.Host.Services.CreateScope()) { var services = serviceScope.ServiceProvider; var service = services.GetRequiredService(); - Assert.True(await service.ObjectExists("avatar", "__default")); - Assert.False(await service.ObjectExists("avatar", "haha")); - Assert.False(await service.ObjectExists("haha", "haha")); + var url = service.GenerateObjectGetUrl("avatar", "__default"); + using (var client = _factory.CreateClient()) + { + var res = await client.GetAsync(url); + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + } } } - */ +#endregion +*/ } } diff --git a/Timeline/Services/TencentCloudCosService.cs b/Timeline/Services/TencentCloudCosService.cs index 8dbd3614..f4358714 100644 --- a/Timeline/Services/TencentCloudCosService.cs +++ b/Timeline/Services/TencentCloudCosService.cs @@ -8,6 +8,7 @@ using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Timeline.Configs; @@ -15,8 +16,21 @@ namespace Timeline.Services { public interface IQCloudCosService { - Task ObjectExists(string bucket, string key); - string GetObjectUrl(string bucket, string key); + /// + /// Test if an object in the bucket exists. + /// + /// The bucket name. + /// The object key. + /// True if exists. False if not. + Task IsObjectExists(string bucket, string key); + + /// + /// Generate a presignated url to access the object. + /// + /// The bucket name. + /// The object key. + /// The presignated url. + string GenerateObjectGetUrl(string bucket, string key); } public class QCloudCosService : IQCloudCosService @@ -32,6 +46,13 @@ namespace Timeline.Services _httpClientFactory = httpClientFactory; } + private const string BucketNamePattern = @"^(([a-z0-9][a-z0-9-]*[a-z0-9])|[a-z0-9])$"; + + public static bool ValidateBucketName(string bucketName) + { + return Regex.IsMatch(bucketName, BucketNamePattern); + } + public class QCloudCredentials { public string SecretId { get; set; } @@ -161,26 +182,29 @@ namespace Timeline.Services return $"{bucket}-{config.AppId}.cos.{config.Region}.myqcloud.com"; } - public async Task ObjectExists(string bucket, string key) + public async Task IsObjectExists(string bucket, string key) { if (bucket == null) throw new ArgumentNullException(nameof(bucket)); if (key == null) throw new ArgumentNullException(nameof(key)); + if (!ValidateBucketName(bucket)) + throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); var client = _httpClientFactory.CreateClient(); var host = GetHost(bucket); + var encodedKey = WebUtility.UrlEncode(key); var request = new HttpRequestMessage(); request.Method = HttpMethod.Head; - request.RequestUri = new Uri($"https://{host}/{key}"); + request.RequestUri = new Uri($"https://{host}/{encodedKey}"); request.Headers.Host = host; request.Headers.Date = DateTimeOffset.Now; request.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo { Method = "head", - Uri = "/" + key, + Uri = "/" + encodedKey, Headers = new Dictionary { ["Host"] = host @@ -205,9 +229,29 @@ namespace Timeline.Services } } - public string GetObjectUrl(string bucket, string key) + public string GenerateObjectGetUrl(string bucket, string key) { - throw new NotImplementedException(); + if (bucket == null) + throw new ArgumentNullException(nameof(bucket)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (!ValidateBucketName(bucket)) + throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); + + var host = GetHost(bucket); + var encodedKey = WebUtility.UrlEncode(key); + + var signature = GenerateSign(GetCredentials(), new RequestInfo + { + Method = "get", + Uri = "/" + encodedKey, + Headers = new Dictionary + { + ["Host"] = host + } + }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(6))); + + return $"https://{host}/{encodedKey}?{signature}"; } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index d1555660..4a47ca0f 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -301,11 +301,11 @@ namespace Timeline.Services public async Task GetAvatarUrl(string username) { - var exists = await _cosService.ObjectExists("avatar", username); + var exists = await _cosService.IsObjectExists("avatar", username); if (exists) - return _cosService.GetObjectUrl("avatar", username); + return _cosService.GenerateObjectGetUrl("avatar", username); else - return _cosService.GetObjectUrl("avatar", "__default"); + return _cosService.GenerateObjectGetUrl("avatar", "__default"); } } } -- cgit v1.2.3 From 484f59f9c954fdced635c24c5ab49840c3022d93 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 30 Apr 2019 19:49:27 +0800 Subject: Add avatar upload function. --- Timeline/Controllers/UserController.cs | 34 ++++++++++++++-- Timeline/Entities/Http/User.cs | 11 ++++++ Timeline/Services/QCloudCosService.cs | 71 ++++++++++++++++++++++++++++++++++ Timeline/Services/UserService.cs | 42 ++++++++++++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index eaa205de..a18e36e9 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; +using System.IO; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Entities.Http; @@ -82,14 +84,38 @@ namespace Timeline.Controllers [HttpGet("user/{username}/avatar"), Authorize] public async Task GetAvatar([FromRoute] string username) { - var existence = (await _userService.GetUser(username)) != null; - if (!existence) - return NotFound(); - var url = await _userService.GetAvatarUrl(username); + if (url == null) + return NotFound(); return Redirect(url); } + [HttpPut("user/{username}/avatar"), Authorize] + [Consumes("image/png", "image/gif", "image/jpeg", "image/svg+xml")] + public async Task PutAvatar([FromRoute] string username, [FromHeader(Name="Content-Type")] string contentType) + { + bool isAdmin = User.IsInRole("admin"); + if (!isAdmin) + { + if (username != User.Identity.Name) + return StatusCode(StatusCodes.Status403Forbidden, PutAvatarResponse.Forbidden); + } + + var stream = new MemoryStream(); + await Request.Body.CopyToAsync(stream); + var result = await _userService.PutAvatar(username, stream.ToArray(), contentType); + switch (result) + { + case PutAvatarResult.Success: + return Ok(PutAvatarResponse.Success); + case PutAvatarResult.UserNotExists: + return BadRequest(PutAvatarResponse.NotExists); + default: + throw new Exception("Unknown put avatar result."); + } + } + + [HttpPost("userop/changepassword"), Authorize] public async Task ChangePassword([FromBody] ChangePasswordRequest request) { diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs index d42ca088..31cafaa3 100644 --- a/Timeline/Entities/Http/User.cs +++ b/Timeline/Entities/Http/User.cs @@ -40,4 +40,15 @@ public static ReturnCodeMessageResponse BadOldPassword { get; } = new ReturnCodeMessageResponse(BadOldPasswordCode, "Old password is wrong."); public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "Username does not exists, please update token."); } + + public static class PutAvatarResponse + { + public const int SuccessCode = 0; + 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."); + } } diff --git a/Timeline/Services/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs index f4358714..078dd37b 100644 --- a/Timeline/Services/QCloudCosService.cs +++ b/Timeline/Services/QCloudCosService.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -24,6 +25,14 @@ namespace Timeline.Services /// True if exists. False if not. Task IsObjectExists(string bucket, string key); + /// + /// Upload an object use put method. + /// + /// The bucket name. + /// The object key. + /// The data to upload. + Task PutObject(string bucket, string key, byte[] data, string contentType); + /// /// Generate a presignated url to access the object. /// @@ -229,6 +238,68 @@ namespace Timeline.Services } } + public async Task PutObject(string bucket, string key, byte[] data, string contentType) + { + if (bucket == null) + throw new ArgumentNullException(nameof(bucket)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (!ValidateBucketName(bucket)) + throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var host = GetHost(bucket); + var encodedKey = WebUtility.UrlEncode(key); + var md5 = Convert.ToBase64String(MD5.Create().ComputeHash(data)); + + const string kContentMD5HeaderName = "Content-MD5"; + const string kContentTypeHeaderName = "Content-Type"; + + var httpRequest = new HttpRequestMessage() + { + Method = HttpMethod.Put, + RequestUri = new Uri($"https://{host}/{encodedKey}") + }; + httpRequest.Headers.Host = host; + httpRequest.Headers.Date = DateTimeOffset.Now; + var httpContent = new ByteArrayContent(data); + httpContent.Headers.Add(kContentMD5HeaderName, md5); + httpRequest.Content = httpContent; + + var signedHeaders = new Dictionary + { + ["Host"] = host, + [kContentMD5HeaderName] = md5 + }; + + if (contentType != null) + { + httpContent.Headers.Add(kContentTypeHeaderName, contentType); + signedHeaders.Add(kContentTypeHeaderName, contentType); + } + + httpRequest.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo + { + Method = "put", + Uri = "/" + encodedKey, + Headers = signedHeaders + }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(10)))); + + var client = _httpClientFactory.CreateClient(); + + try + { + var response = await client.SendAsync(httpRequest); + if (!response.IsSuccessStatusCode) + throw new Exception($"Not success status code. {response.ToString()}"); + } + catch (Exception e) + { + _logger.LogError(e, "An error occured when test a cos object existence."); + } + } + public string GenerateObjectGetUrl(string bucket, string key) { if (bucket == null) diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 4a47ca0f..9ebf2668 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using System; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; @@ -65,6 +66,18 @@ namespace Timeline.Services BadOldPassword } + public enum PutAvatarResult + { + /// + /// Success to upload avatar. + /// + Success, + /// + /// The user does not exists. + /// + UserNotExists + } + public interface IUserService { /// @@ -141,7 +154,14 @@ namespace Timeline.Services /// if old password is wrong. Task ChangePassword(string username, string oldPassword, string newPassword); + /// + /// Get the true avatar url of a user. + /// + /// The name of user. + /// The url if user exists. Null if user does not exist. Task GetAvatarUrl(string username); + + Task PutAvatar(string username, byte[] data, string mimeType); } public class UserService : IUserService @@ -301,11 +321,33 @@ namespace Timeline.Services public async Task GetAvatarUrl(string username) { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + if ((await GetUser(username)) == null) + return null; + var exists = await _cosService.IsObjectExists("avatar", username); if (exists) return _cosService.GenerateObjectGetUrl("avatar", username); else return _cosService.GenerateObjectGetUrl("avatar", "__default"); } + + public async Task PutAvatar(string username, byte[] data, string mimeType) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + if (mimeType == null) + throw new ArgumentNullException(nameof(mimeType)); + + if ((await GetUser(username)) == null) + return PutAvatarResult.UserNotExists; + + await _cosService.PutObject("avatar", username, data, mimeType); + return PutAvatarResult.Success; + } } } -- cgit v1.2.3 From a04bcb5971872e7dbc079de9337875e73f7642dc Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 30 Apr 2019 20:00:42 +0800 Subject: Throw exception in cos service. --- Timeline/Services/QCloudCosService.cs | 3 ++- Timeline/Services/UserService.cs | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'Timeline/Services/UserService.cs') diff --git a/Timeline/Services/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs index 078dd37b..b37631e5 100644 --- a/Timeline/Services/QCloudCosService.cs +++ b/Timeline/Services/QCloudCosService.cs @@ -234,7 +234,7 @@ namespace Timeline.Services catch (Exception e) { _logger.LogError(e, "An error occured when test a cos object existence."); - return false; + throw; } } @@ -297,6 +297,7 @@ namespace Timeline.Services catch (Exception e) { _logger.LogError(e, "An error occured when test a cos object existence."); + throw; } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 9ebf2668..8ab3bc54 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -161,6 +161,14 @@ namespace Timeline.Services /// The url if user exists. Null if user does not exist. Task GetAvatarUrl(string username); + /// + /// Put a avatar of a user. + /// + /// The name of user. + /// The data of avatar image. + /// The mime type of the image. + /// Return if success. + /// Return if user does not exist. Task PutAvatar(string username, byte[] data, string mimeType); } -- cgit v1.2.3