From 647006822f01a53dade5ea040210059a98a43196 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 18 Aug 2019 19:15:44 +0800 Subject: Add avatar controller. --- Timeline/Controllers/UserAvatarController.cs | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 Timeline/Controllers/UserAvatarController.cs (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs new file mode 100644 index 00000000..f61fd54a --- /dev/null +++ b/Timeline/Controllers/UserAvatarController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; + +namespace Timeline.Controllers +{ + [ApiController] + public class UserAvatarController : Controller + { + public static class ErrorCodes + { + public const int Get_UserNotExist = -1001; + + public const int Put_UserNotExist = -2001; + } + + private readonly ILogger _logger; + + private readonly IUserAvatarService _service; + + public UserAvatarController(ILogger logger, IUserAvatarService service) + { + _logger = logger; + _service = service; + } + + [HttpGet("users/{username}/avatar")] + public async Task Get(string username) + { + try + { + var avatar = await _service.GetAvatar(username); + return File(avatar.Data, avatar.Type); + } + catch (UserNotExistException) + { + _logger.LogInformation($"Attempt to get a avatar of a non-existent user failed. Username: {username} ."); + return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist.")); + } + } + + [HttpPut("users/{username}/avatar")] + [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] + public async Task Put(string username) + { + try + { + var data = new byte[Convert.ToInt32(Request.ContentLength)]; + await Request.Body.ReadAsync(data, 0, data.Length); + + await _service.SetAvatar(username, new Avatar + { + Data = data, + Type = Request.ContentType + }); + + _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} . Mime Type: {Request.ContentType} ."); + return Ok(); + } + catch (UserNotExistException) + { + _logger.LogInformation($"Attempt to put a avatar of a non-existent user failed. Username: {username} ."); + return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist.")); + } + } + } +} -- cgit v1.2.3 From bc832053bdad644a0a86ded0173f3f47c0159018 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 18 Aug 2019 23:08:05 +0800 Subject: Develop user avatar controller. --- Timeline/Authenticate/PrincipalExtensions.cs | 13 ++++++++ Timeline/Controllers/UserAvatarController.cs | 45 ++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Timeline/Authenticate/PrincipalExtensions.cs (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline/Authenticate/PrincipalExtensions.cs b/Timeline/Authenticate/PrincipalExtensions.cs new file mode 100644 index 00000000..fa39ea89 --- /dev/null +++ b/Timeline/Authenticate/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal; +using Timeline.Entities; + +namespace Timeline.Authenticate +{ + public static class PrincipalExtensions + { + public static bool IsAdmin(this IPrincipal principal) + { + return principal.IsInRole(UserRoles.Admin); + } + } +} diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index f61fd54a..6dc767df 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -1,7 +1,10 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; +using Timeline.Authenticate; using Timeline.Models.Http; using Timeline.Services; @@ -15,6 +18,10 @@ namespace Timeline.Controllers public const int Get_UserNotExist = -1001; public const int Put_UserNotExist = -2001; + public const int Put_Forbid = -2002; + + public const int Delete_UserNotExist = -3001; + public const int Delete_Forbid = -3002; } private readonly ILogger _logger; @@ -28,6 +35,7 @@ namespace Timeline.Controllers } [HttpGet("users/{username}/avatar")] + [Authorize] public async Task Get(string username) { try @@ -43,9 +51,17 @@ namespace Timeline.Controllers } [HttpPut("users/{username}/avatar")] + [Authorize] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put(string username) { + if (!User.IsAdmin() && User.Identity.Name != username) + { + _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + return StatusCode(StatusCodes.Status403Forbidden, + new CommonResponse(ErrorCodes.Put_Forbid, "Normal user can't change other's avatar.")); + } + try { var data = new byte[Convert.ToInt32(Request.ContentLength)]; @@ -57,7 +73,7 @@ namespace Timeline.Controllers Type = Request.ContentType }); - _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} . Mime Type: {Request.ContentType} ."); + _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} ."); return Ok(); } catch (UserNotExistException) @@ -66,5 +82,30 @@ namespace Timeline.Controllers return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist.")); } } + + [HttpDelete("users/{username}/avatar")] + [Authorize] + public async Task Delete(string username) + { + if (!User.IsAdmin() && User.Identity.Name != username) + { + _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + return StatusCode(StatusCodes.Status403Forbidden, + new CommonResponse(ErrorCodes.Delete_Forbid, "Normal user can't delete other's avatar.")); + } + + try + { + await _service.SetAvatar(username, null); + + _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} ."); + return Ok(); + } + catch (UserNotExistException) + { + _logger.LogInformation($"Attempt to delete a avatar of a non-existent user failed. Username: {username} ."); + return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist.")); + } + } } } -- cgit v1.2.3 From e946c2e546efae29112628c0e6d42dc145605f09 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 19 Aug 2019 00:21:38 +0800 Subject: Improve avatar validation. --- Timeline/Controllers/UserAvatarController.cs | 36 ++++++++++++++++++++---- Timeline/Services/UserAvatarService.cs | 41 ++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 17 deletions(-) (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 6dc767df..710ca764 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -19,9 +19,28 @@ namespace Timeline.Controllers public const int Put_UserNotExist = -2001; public const int Put_Forbid = -2002; + public const int Put_BadFormat_CantDecode = -2011; + public const int Put_BadFormat_UnmatchedFormat = -2012; + public const int Put_BadFormat_BadSize = -2013; public const int Delete_UserNotExist = -3001; public const int Delete_Forbid = -3002; + + + public static int From(AvatarDataException.ErrorReason error) + { + switch (error) + { + case AvatarDataException.ErrorReason.CantDecode: + return Put_BadFormat_CantDecode; + case AvatarDataException.ErrorReason.UnmatchedFormat: + return Put_BadFormat_UnmatchedFormat; + case AvatarDataException.ErrorReason.BadSize: + return Put_BadFormat_BadSize; + default: + throw new Exception("Unknown AvatarDataException.ErrorReason value."); + } + } } private readonly ILogger _logger; @@ -43,9 +62,9 @@ namespace Timeline.Controllers var avatar = await _service.GetAvatar(username); return File(avatar.Data, avatar.Type); } - catch (UserNotExistException) + catch (UserNotExistException e) { - _logger.LogInformation($"Attempt to get a avatar of a non-existent user failed. Username: {username} ."); + _logger.LogInformation(e, $"Attempt to get a avatar of a non-existent user failed. Username: {username} ."); return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist.")); } } @@ -76,11 +95,16 @@ namespace Timeline.Controllers _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} ."); return Ok(); } - catch (UserNotExistException) + catch (UserNotExistException e) { - _logger.LogInformation($"Attempt to put a avatar of a non-existent user failed. Username: {username} ."); + _logger.LogInformation(e, $"Attempt to put a avatar of a non-existent user failed. Username: {username} ."); return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist.")); } + catch (AvatarDataException e) + { + _logger.LogInformation(e, $"Attempt to put a avatar of a bad format failed. Username: {username} ."); + return BadRequest(new CommonResponse(ErrorCodes.From(e.Error), "Bad format.")); + } } [HttpDelete("users/{username}/avatar")] @@ -101,9 +125,9 @@ namespace Timeline.Controllers _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} ."); return Ok(); } - catch (UserNotExistException) + catch (UserNotExistException e) { - _logger.LogInformation($"Attempt to delete a avatar of a non-existent user failed. Username: {username} ."); + _logger.LogInformation(e, $"Attempt to delete a avatar of a non-existent user failed. Username: {username} ."); return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist.")); } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 2a73cde5..dd0e5e7c 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -24,12 +24,29 @@ namespace Timeline.Services [Serializable] public class AvatarDataException : Exception { - public AvatarDataException(Avatar avatar, string message) : base(message) { Avatar = avatar; } - public AvatarDataException(Avatar avatar, string message, Exception inner) : base(message, inner) { Avatar = avatar; } + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not a square. + /// + BadSize + } + + public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; } + public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; } protected AvatarDataException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + public ErrorReason Error { get; set; } public Avatar Avatar { get; set; } } @@ -49,7 +66,12 @@ namespace Timeline.Services public interface IUserAvatarValidator { - Task<(bool valid, string message)> Validate(Avatar avatar); + /// + /// Validate a avatar's format and size info. + /// + /// The avatar to validate. + /// Thrown when validation failed. + Task Validate(Avatar avatar); } public interface IUserAvatarService @@ -96,7 +118,7 @@ namespace Timeline.Services public class UserAvatarValidator : IUserAvatarValidator { - public Task<(bool valid, string message)> Validate(Avatar avatar) + public Task Validate(Avatar avatar) { return Task.Run(() => { @@ -105,15 +127,14 @@ namespace Timeline.Services using (var image = Image.Load(avatar.Data, out IImageFormat format)) { if (!format.MimeTypes.Contains(avatar.Type)) - return (false, "Image's actual mime type is not the specified one."); + throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one."); if (image.Width != image.Height) - return (false, "Image is not a square, aka width is not equal to height."); + throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height."); } - return (true, "A good avatar."); } catch (UnknownImageFormatException e) { - return (false, $"Failed to decode image. Exception: {e} ."); + throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e); } }); } @@ -196,9 +217,7 @@ namespace Timeline.Services } else { - (bool valid, string message) = await _avatarValidator.Validate(avatar); - if (!valid) - throw new AvatarDataException(avatar, $"Failed to validate image. {message}"); + await _avatarValidator.Validate(avatar); if (avatarEntity == null) { -- cgit v1.2.3