From 52acf41e331ddbd66befed4692c804b754ba7d5c Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 30 Jan 2020 20:26:52 +0800 Subject: ... --- Timeline/Auth/MyAuthenticationHandler.cs | 1 - Timeline/Controllers/ControllerAuthExtensions.cs | 15 + Timeline/Controllers/PersonalTimelineController.cs | 111 +++---- .../Controllers/Testing/TestingI18nController.cs | 36 -- Timeline/Controllers/TokenController.cs | 18 +- Timeline/Controllers/UserAvatarController.cs | 89 ++--- Timeline/Controllers/UserController.cs | 58 ++-- Timeline/Entities/DatabaseContext.cs | 2 - Timeline/Entities/TimelineEntity.cs | 2 +- Timeline/Filters/Header.cs | 2 - Timeline/Filters/Timeline.cs | 1 - Timeline/Filters/User.cs | 68 ---- Timeline/Formatters/StringInputFormatter.cs | 1 - Timeline/GlobalSuppressions.cs | 1 + Timeline/Helpers/InvalidModelResponseFactory.cs | 1 - .../Models/Converters/JsonDateTimeConverter.cs | 1 - Timeline/Models/Http/ErrorResponse.cs | 18 +- Timeline/Models/Http/Timeline.cs | 42 --- Timeline/Models/Http/TimelineCommon.cs | 44 +++ Timeline/Models/Http/TimelineController.cs | 20 ++ Timeline/Models/Http/Token.cs | 32 -- Timeline/Models/Http/TokenController.cs | 32 ++ Timeline/Models/Http/UserController.cs | 26 ++ Timeline/Models/Http/UserInfo.cs | 58 ++++ Timeline/Models/Timeline.cs | 55 ---- Timeline/Models/Validation/NicknameValidator.cs | 1 - Timeline/Models/Validation/UsernameValidator.cs | 1 - .../Testing/TestingI18nController.Designer.cs | 72 ---- .../Controllers/Testing/TestingI18nController.resx | 123 ------- .../Testing/TestingI18nController.zh.resx | 123 ------- Timeline/Resources/Messages.Designer.cs | 27 +- Timeline/Resources/Messages.resx | 9 +- Timeline/Resources/Services/Exception.Designer.cs | 49 +-- Timeline/Resources/Services/Exception.resx | 17 +- .../Resources/Services/TimelineService.Designer.cs | 81 +++++ Timeline/Resources/Services/TimelineService.resx | 126 +++++++ Timeline/Resources/Services/UserCache.Designer.cs | 99 ------ Timeline/Resources/Services/UserCache.resx | 132 -------- .../Services/UserDetailService.Designer.cs | 99 ------ Timeline/Resources/Services/UserDetailService.resx | 132 -------- Timeline/Services/ConfictException.cs | 21 -- Timeline/Services/ConflictException.cs | 21 ++ Timeline/Services/DatabaseExtensions.cs | 36 -- Timeline/Services/TimelineAlreadyExistException.cs | 17 - .../TimelineMemberOperationUserException.cs | 37 --- .../Services/TimelineNameBadFormatException.cs | 21 -- Timeline/Services/TimelineService.cs | 363 +++++++-------------- Timeline/Services/User.cs | 33 +- Timeline/Services/UserAvatarService.cs | 48 +-- Timeline/Services/UserRoleConvert.cs | 1 - Timeline/Services/UserService.cs | 36 +- Timeline/Services/UserTokenService.cs | 1 - Timeline/Startup.cs | 12 +- Timeline/Timeline.csproj | 60 ++-- 54 files changed, 790 insertions(+), 1742 deletions(-) delete mode 100644 Timeline/Controllers/Testing/TestingI18nController.cs delete mode 100644 Timeline/Filters/User.cs delete mode 100644 Timeline/Models/Http/Timeline.cs create mode 100644 Timeline/Models/Http/TimelineCommon.cs create mode 100644 Timeline/Models/Http/TimelineController.cs delete mode 100644 Timeline/Models/Http/Token.cs create mode 100644 Timeline/Models/Http/TokenController.cs create mode 100644 Timeline/Models/Http/UserInfo.cs delete mode 100644 Timeline/Models/Timeline.cs delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.resx delete mode 100644 Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx create mode 100644 Timeline/Resources/Services/TimelineService.Designer.cs create mode 100644 Timeline/Resources/Services/TimelineService.resx delete mode 100644 Timeline/Resources/Services/UserCache.Designer.cs delete mode 100644 Timeline/Resources/Services/UserCache.resx delete mode 100644 Timeline/Resources/Services/UserDetailService.Designer.cs delete mode 100644 Timeline/Resources/Services/UserDetailService.resx delete mode 100644 Timeline/Services/ConfictException.cs create mode 100644 Timeline/Services/ConflictException.cs delete mode 100644 Timeline/Services/DatabaseExtensions.cs delete mode 100644 Timeline/Services/TimelineAlreadyExistException.cs delete mode 100644 Timeline/Services/TimelineMemberOperationUserException.cs delete mode 100644 Timeline/Services/TimelineNameBadFormatException.cs (limited to 'Timeline') diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs index e6b26c4b..3c97c329 100644 --- a/Timeline/Auth/MyAuthenticationHandler.cs +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Timeline.Models; using Timeline.Services; using static Timeline.Resources.Authentication.AuthHandler; diff --git a/Timeline/Controllers/ControllerAuthExtensions.cs b/Timeline/Controllers/ControllerAuthExtensions.cs index 81fd2428..90da8a93 100644 --- a/Timeline/Controllers/ControllerAuthExtensions.cs +++ b/Timeline/Controllers/ControllerAuthExtensions.cs @@ -26,5 +26,20 @@ namespace Timeline.Controllers throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); } + + public static long? GetOptionalUserId(this ControllerBase controller) + { + if (controller.User == null) + return null; + + var claim = controller.User.FindFirst(ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException("Failed to get user id because User has no NameIdentifier claim."); + + if (long.TryParse(claim.Value, out var value)) + return value; + + throw new InvalidOperationException("Failed to get user id because NameIdentifier claim is not a number."); + } } } diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 2c70fad1..27618c41 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -4,45 +4,21 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Threading.Tasks; -using Timeline.Auth; using Timeline.Filters; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; using Timeline.Services; -using static Timeline.Resources.Controllers.TimelineController; -using static Timeline.Resources.Messages; namespace Timeline.Controllers { [ApiController] + [CatchTimelineNotExistException] public class PersonalTimelineController : Controller { private readonly ILogger _logger; private readonly IPersonalTimelineService _service; - private bool IsAdmin() - { - if (User != null) - { - return User.IsAdministrator(); - } - return false; - } - - private string? GetAuthUsername() - { - if (User == null) - { - return null; - } - else - { - return User.Identity.Name; - } - } - public PersonalTimelineController(ILogger logger, IPersonalTimelineService service) { _logger = logger; @@ -50,17 +26,15 @@ namespace Timeline.Controllers } [HttpGet("users/{username}/timeline")] - [CatchTimelineNotExistException] public async Task> TimelineGet([FromRoute][Username] string username) { return await _service.GetTimeline(username); } [HttpGet("users/{username}/timeline/posts")] - [CatchTimelineNotExistException] public async Task>> PostListGet([FromRoute][Username] string username) { - if (!IsAdmin() && !await _service.HasReadPermission(username, GetAuthUsername())) + if (!this.IsAdministrator() && !await _service.HasReadPermission(username, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } @@ -68,77 +42,88 @@ namespace Timeline.Controllers return await _service.GetPosts(username); } - [HttpPost("users/{username}/timeline/postop/create")] + [HttpPost("users/{username}/timeline/posts")] [Authorize] - [CatchTimelineNotExistException] - public async Task> PostOperationCreate([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) + public async Task> PostPost([FromRoute][Username] string username, [FromBody] TimelinePostCreateRequest body) { - if (!IsAdmin() && !await _service.IsMemberOf(username, GetAuthUsername()!)) + var id = this.GetUserId(); + if (!this.IsAdministrator() && !await _service.IsMemberOf(username, id)) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var res = await _service.CreatePost(username, User.Identity.Name!, body.Content, body.Time); + var res = await _service.CreatePost(username, id, body.Content, body.Time); return res; } - [HttpPost("users/{username}/timeline/postop/delete")] + [HttpDelete("users/{username}/timeline/posts/{id}")] [Authorize] - [CatchTimelineNotExistException] - public async Task PostOperationDelete([FromRoute][Username] string username, [FromBody] TimelinePostDeleteRequest body) + public async Task PostDelete([FromRoute][Username] string username, [FromRoute] long id) { try { - var postId = body.Id!.Value; - if (!IsAdmin() && !await _service.HasPostModifyPermission(username, postId, GetAuthUsername()!)) + if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(username, id, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - await _service.DeletePost(username, postId); + await _service.DeletePost(username, id); + return Ok(CommonDeleteResponse.Delete()); } catch (TimelinePostNotExistException) { - return BadRequest(ErrorResponse.TimelineController.PostOperationDelete_NotExist()); + return Ok(CommonDeleteResponse.NotExist()); } - return Ok(); } - [HttpPost("users/{username}/timeline/op/property")] + [HttpPatch("users/{username}/timeline")] [Authorize] - [SelfOrAdmin] - [CatchTimelineNotExistException] - public async Task TimelineChangeProperty([FromRoute][Username] string username, [FromBody] TimelinePropertyChangeRequest body) + public async Task TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body) { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } await _service.ChangeProperty(username, body); return Ok(); } - [HttpPost("users/{username}/timeline/op/member")] + [HttpPut("users/{username}/timeline/members/{member}")] [Authorize] - [SelfOrAdmin] - [CatchTimelineNotExistException] - public async Task TimelineChangeMember([FromRoute][Username] string username, [FromBody] TimelineMemberChangeRequest body) + public async Task TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member) { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + try { - await _service.ChangeMember(username, body.Add, body.Remove); + await _service.ChangeMember(username, new List { member }, null); return Ok(); } - catch (TimelineMemberOperationUserException e) + catch (UserNotExistException) { - if (e.InnerException is UsernameBadFormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel( - TimelineController_ChangeMember_UsernameBadFormat, e.Index, e.Operation)); - } - else if (e.InnerException is UserNotExistException) - { - return BadRequest(ErrorResponse.UserCommon.CustomMessage_NotExist( - TimelineController_ChangeMember_UserNotExist, e.Index, e.Operation)); - } + return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + } + } + + [HttpDelete("users/{username}/timeline/members/{member}")] + [Authorize] + public async Task TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(User.Identity.Name == username)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } - _logger.LogError(e, LogUnknownTimelineMemberOperationUserException); - throw; + try + { + await _service.ChangeMember(username, null, new List { member }); + return Ok(CommonDeleteResponse.Delete()); + } + catch (UserNotExistException) + { + return Ok(CommonDeleteResponse.NotExist()); } } } diff --git a/Timeline/Controllers/Testing/TestingI18nController.cs b/Timeline/Controllers/Testing/TestingI18nController.cs deleted file mode 100644 index febb56a5..00000000 --- a/Timeline/Controllers/Testing/TestingI18nController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Localization; - -// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 - -namespace Timeline.Controllers.Testing -{ - [Route("testing/i18n")] - [ApiController] - public class TestingI18nController : Controller - { - private readonly IStringLocalizer _stringLocalizer; - - public TestingI18nController(IStringLocalizer stringLocalizer) - { - _stringLocalizer = stringLocalizer; - } - - [HttpGet("direct")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] - public ActionResult Direct() - { - return Resources.Controllers.Testing.TestingI18nController.TestString; - } - - [HttpGet("localizer")] - public ActionResult Localizer() - { - return _stringLocalizer["TestString"].Value; - } - } -} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 9724c1a6..a7f5fbde 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -5,7 +6,6 @@ using System; using System.Globalization; using System.Threading.Tasks; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; using static Timeline.Resources.Controllers.TokenController; @@ -20,20 +20,14 @@ namespace Timeline.Controllers private readonly ILogger _logger; private readonly IClock _clock; - private static Models.Http.User CreateUserFromUserInfo(Models.User userInfo) - { - return new Models.Http.User - { - Username = userInfo.Username, - Administrator = userInfo.Administrator - }; - } + private readonly IMapper _mapper; - public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock) + public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) { _userTokenManager = userTokenManager; _logger = logger; _clock = clock; + _mapper = mapper; } [HttpPost("create")] @@ -65,7 +59,7 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Token = result.Token, - User = CreateUserFromUserInfo(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -100,7 +94,7 @@ namespace Timeline.Controllers ("Username", result.Username), ("Token", request.Token))); return Ok(new VerifyTokenResponse { - User = CreateUserFromUserInfo(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 62f1d78c..ab0ad8e7 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -21,11 +21,13 @@ namespace Timeline.Controllers { private readonly ILogger _logger; + private readonly IUserService _userService; private readonly IUserAvatarService _service; - public UserAvatarController(ILogger logger, IUserAvatarService service) + public UserAvatarController(ILogger logger, IUserService userService, IUserAvatarService service) { _logger = logger; + _userService = userService; _service = service; } @@ -33,46 +35,50 @@ namespace Timeline.Controllers [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)] public async Task Get([FromRoute][Username] string username) { - const string IfNonMatchHeaderKey = "If-None-Match"; - + long id; try { - var eTagValue = $"\"{await _service.GetAvatarETag(username)}\""; - var eTag = new EntityTagHeaderValue(eTagValue); - - if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) - { - if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - { - _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, - ("Username", username), ("If-None-Match", value))); - return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); - } - - if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) - { - Response.Headers.Add("ETag", eTagValue); - _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username))); - return StatusCode(StatusCodes.Status304NotModified); - } - } - - var avatarInfo = await _service.GetAvatar(username); - var avatar = avatarInfo.Avatar; - - _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username))); - return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); + id = await _userService.GetUserIdByUsername(username); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogGetUserNotExist, ("Username", username))); return NotFound(ErrorResponse.UserCommon.NotExist()); } + + const string IfNonMatchHeaderKey = "If-None-Match"; + + var eTagValue = $"\"{await _service.GetAvatarETag(id)}\""; + var eTag = new EntityTagHeaderValue(eTagValue); + + if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) + { + if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) + { + _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, + ("Username", username), ("If-None-Match", value))); + return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); + } + + if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) + { + Response.Headers.Add("ETag", eTagValue); + _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username))); + return StatusCode(StatusCodes.Status304NotModified); + } + } + + var avatarInfo = await _service.GetAvatar(id); + var avatar = avatarInfo.Avatar; + + _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username))); + return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); + } [HttpPut("users/{username}/avatar")] [Authorize] - [RequireContentType, RequireContentLength] + [RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] public async Task Put([FromRoute][Username] string username) { @@ -87,6 +93,17 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + long id; + try + { + id = await _userService.GetUserIdByUsername(username); + } + catch (UserNotExistException e) + { + _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); + return BadRequest(ErrorResponse.UserCommon.NotExist()); + } + try { var data = new byte[contentLength]; @@ -99,7 +116,7 @@ namespace Timeline.Controllers if (await Request.Body.ReadAsync(extraByte) != 0) return BadRequest(ErrorResponse.Common.Content.UnmatchedLength_Bigger()); - await _service.SetAvatar(username, new Avatar + await _service.SetAvatar(id, new Avatar { Data = data, Type = Request.ContentType @@ -109,11 +126,6 @@ namespace Timeline.Controllers ("Username", username), ("Mime Type", Request.ContentType))); return Ok(); } - catch (UserNotExistException e) - { - _logger.LogInformation(e, Log.Format(LogPutUserNotExist, ("Username", username))); - return BadRequest(ErrorResponse.UserCommon.NotExist()); - } catch (AvatarFormatException e) { _logger.LogInformation(e, Log.Format(LogPutUserBadFormat, ("Username", username))); @@ -139,16 +151,19 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } + long id; try { - await _service.SetAvatar(username, null); - return Ok(); + id = await _userService.GetUserIdByUsername(username); } catch (UserNotExistException e) { _logger.LogInformation(e, Log.Format(LogDeleteNotExist, ("Username", username))); return BadRequest(ErrorResponse.UserCommon.NotExist()); } + + await _service.SetAvatar(id, null); + return Ok(); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 4c585198..400a518c 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,30 +18,40 @@ namespace Timeline.Controllers [ApiController] public class UserController : Controller { - private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IMapper _mapper; - public UserController(ILogger logger, IUserService userService) + public UserController(ILogger logger, IUserService userService, IMapper mapper) { _logger = logger; _userService = userService; + _mapper = mapper; + } + + private IUserInfo ConvertToUserInfo(User user, bool administrator) + { + if (administrator) + return _mapper.Map(user); + else + return _mapper.Map(user); } [HttpGet("users")] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); - return Ok(users.Select(u => u.EraseSecretAndFinalFill(Url, this.IsAdministrator())).ToArray()); + var administrator = this.IsAdministrator(); + return Ok(users.Select(u => ConvertToUserInfo(u, administrator)).ToArray()); } [HttpGet("users/{username}")] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { var user = await _userService.GetUserByUsername(username); - return Ok(user.EraseSecretAndFinalFill(Url, this.IsAdministrator())); + return Ok(ConvertToUserInfo(user, this.IsAdministrator())); } catch (UserNotExistException e) { @@ -52,22 +63,11 @@ namespace Timeline.Controllers [HttpPatch("users/{username}"), Authorize] public async Task Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) { - static User Convert(UserPatchRequest body) - { - return new User - { - Username = body.Username, - Password = body.Password, - Administrator = body.Administrator, - Nickname = body.Nickname - }; - } - if (this.IsAdministrator()) { try { - await _userService.ModifyUser(username, Convert(body)); + await _userService.ModifyUser(username, _mapper.Map(body)); return Ok(); } catch (UserNotExistException e) @@ -75,6 +75,10 @@ namespace Timeline.Controllers _logger.LogInformation(e, Log.Format(LogPatchUserNotExist, ("Username", username))); return NotFound(ErrorResponse.UserCommon.NotExist()); } + catch (ConflictException) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } } else { @@ -94,7 +98,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.CustomMessage_Forbid(UserController_Patch_Forbid_Administrator)); - await _userService.ModifyUser(this.GetUserId(), Convert(body)); + await _userService.ModifyUser(this.GetUserId(), _mapper.Map(body)); return Ok(); } } @@ -113,10 +117,18 @@ namespace Timeline.Controllers } } - [HttpPost("userop/create"), AdminAuthorize] - public async Task CreateUser([FromBody] User body) + [HttpPost("userop/createuser"), AdminAuthorize] + public async Task CreateUser([FromBody] CreateUserRequest body) { - + try + { + await _userService.CreateUser(_mapper.Map(body)); + return Ok(); + } + catch (ConflictException) + { + return BadRequest(ErrorResponse.UserController.UsernameConflict()); + } } [HttpPost("userop/changepassword"), Authorize] @@ -133,7 +145,7 @@ namespace Timeline.Controllers ("Username", User.Identity.Name), ("Old Password", request.OldPassword))); return BadRequest(ErrorResponse.UserController.ChangePassword_BadOldPassword()); } - // User can't be non-existent or the token is bad. + // User can't be non-existent or the token is bad. } } } diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ac4ad7b2..cac33379 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -10,7 +10,6 @@ namespace Timeline.Entities } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); @@ -19,7 +18,6 @@ namespace Timeline.Entities public DbSet Users { get; set; } = default!; public DbSet UserAvatars { get; set; } = default!; - public DbSet UserDetails { get; set; } = default!; public DbSet Timelines { get; set; } = default!; public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 2bfd6107..c50fe6dd 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Timeline.Models; +using Timeline.Models.Http; namespace Timeline.Entities { diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs index 843a619d..0db11faf 100644 --- a/Timeline/Filters/Header.cs +++ b/Timeline/Filters/Header.cs @@ -6,7 +6,6 @@ namespace Timeline.Filters { public class RequireContentTypeAttribute : ActionFilterAttribute { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentType == null) @@ -31,7 +30,6 @@ namespace Timeline.Filters public bool RequireNonZero { get; set; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentLength == null) diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index bc142db0..729dbec7 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -7,7 +7,6 @@ namespace Timeline.Filters { public class CatchTimelineNotExistExceptionAttribute : ExceptionFilterAttribute { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnException(ExceptionContext context) { if (context.Exception is TimelineNotExistException e) diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs deleted file mode 100644 index 12ed6155..00000000 --- a/Timeline/Filters/User.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using Timeline.Auth; -using Timeline.Models.Http; -using Timeline.Services; -using static Timeline.Resources.Filters; - -namespace Timeline.Filters -{ - public class SelfOrAdminAttribute : ActionFilterAttribute - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] - public override void OnActionExecuting(ActionExecutingContext context) - { - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - var user = context.HttpContext.User; - - if (user == null) - { - logger.LogError(LogSelfOrAdminNoUser); - return; - } - - if (context.ModelState.TryGetValue("username", out var model)) - { - if (model.RawValue is string username) - { - if (!user.IsAdministrator() && user.Identity.Name != username) - { - context.Result = new ObjectResult(ErrorResponse.Common.Forbid()) - { StatusCode = StatusCodes.Status403Forbidden }; - } - } - else - { - logger.LogError(LogSelfOrAdminUsernameNotString); - } - } - else - { - logger.LogError(LogSelfOrAdminNoUsername); - } - } - } - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")] - public override void OnException(ExceptionContext context) - { - if (context.Exception is UserNotExistException) - { - var body = ErrorResponse.UserCommon.NotExist(); - - if (context.HttpContext.Request.Method == "GET") - context.Result = new NotFoundObjectResult(body); - else - context.Result = new BadRequestObjectResult(body); - } - } - } -} diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs index 90847e36..b1924268 100644 --- a/Timeline/Formatters/StringInputFormatter.cs +++ b/Timeline/Formatters/StringInputFormatter.cs @@ -15,7 +15,6 @@ namespace Timeline.Formatters SupportedEncodings.Add(Encoding.UTF8); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) { var request = context.HttpContext.Request; diff --git a/Timeline/GlobalSuppressions.cs b/Timeline/GlobalSuppressions.cs index d27b3c16..2b0da576 100644 --- a/Timeline/GlobalSuppressions.cs +++ b/Timeline/GlobalSuppressions.cs @@ -11,3 +11,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Generated error response identifiers.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "Generated error response.", Scope = "type", Target = "Timeline.Models.Http.ErrorResponse")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Adundant")] diff --git a/Timeline/Helpers/InvalidModelResponseFactory.cs b/Timeline/Helpers/InvalidModelResponseFactory.cs index 71ee44a9..9b253e7d 100644 --- a/Timeline/Helpers/InvalidModelResponseFactory.cs +++ b/Timeline/Helpers/InvalidModelResponseFactory.cs @@ -6,7 +6,6 @@ namespace Timeline.Helpers { public static class InvalidModelResponseFactory { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public static IActionResult Factory(ActionContext context) { var modelState = context.ModelState; diff --git a/Timeline/Models/Converters/JsonDateTimeConverter.cs b/Timeline/Models/Converters/JsonDateTimeConverter.cs index 69af53c1..ef129a01 100644 --- a/Timeline/Models/Converters/JsonDateTimeConverter.cs +++ b/Timeline/Models/Converters/JsonDateTimeConverter.cs @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; namespace Timeline.Models.Converters { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public class JsonDateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index 6a53e0c3..87516638 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -184,14 +184,14 @@ namespace Timeline.Models.Http public static class UserController { - public static CommonResponse ChangeUsername_Conflict(params object?[] formatArgs) + public static CommonResponse UsernameConflict(params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(UserController_ChangeUsername_Conflict, formatArgs)); + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(UserController_UsernameConflict, formatArgs)); } - public static CommonResponse CustomMessage_ChangeUsername_Conflict(string message, params object?[] formatArgs) + public static CommonResponse CustomMessage_UsernameConflict(string message, params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.UserController.ChangeUsername_Conflict, string.Format(message, formatArgs)); + return new CommonResponse(ErrorCodes.UserController.UsernameConflict, string.Format(message, formatArgs)); } public static CommonResponse ChangePassword_BadOldPassword(params object?[] formatArgs) @@ -244,18 +244,18 @@ namespace Timeline.Models.Http public static class TimelineController { - public static CommonResponse PostOperationDelete_NotExist(params object?[] formatArgs) + public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(TimelineController_PostOperationDelete_NotExist, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); } - public static CommonResponse CustomMessage_PostOperationDelete_NotExist(string message, params object?[] formatArgs) + public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.PostOperationDelete_NotExist, string.Format(message, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); } } } -} +} \ No newline at end of file diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs deleted file mode 100644 index 3029434e..00000000 --- a/Timeline/Models/Http/Timeline.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Timeline.Models.Http -{ - public class TimelinePostCreateRequest - { - [Required(AllowEmptyStrings = true)] - public string Content { get; set; } = default!; - - public DateTime? Time { get; set; } - } - - public class TimelinePostCreateResponse - { - public long Id { get; set; } - - public DateTime Time { get; set; } - } - - public class TimelinePostDeleteRequest - { - [Required] - public long? Id { get; set; } - } - - public class TimelinePropertyChangeRequest - { - public string? Description { get; set; } - - public TimelineVisibility? Visibility { get; set; } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] - public class TimelineMemberChangeRequest - { - public List? Add { get; set; } - - public List? Remove { get; set; } - } -} diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs new file mode 100644 index 00000000..febb8186 --- /dev/null +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models.Http +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + + public class TimelinePostInfo + { + public long Id { get; set; } + public string Content { get; set; } = default!; + public DateTime Time { get; set; } + public UserInfo Author { get; set; } = default!; + public DateTime LastUpdated { get; set; } = default!; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] + public class BaseTimelineInfo + { + public string Description { get; set; } = default!; + public UserInfo Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } + public List Members { get; set; } = default!; + } + + public class TimelineInfo : BaseTimelineInfo + { + public string Name { get; set; } = default!; + } +} diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs new file mode 100644 index 00000000..f9a4d3e5 --- /dev/null +++ b/Timeline/Models/Http/TimelineController.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class TimelinePostCreateRequest + { + [Required(AllowEmptyStrings = true)] + public string Content { get; set; } = default!; + + public DateTime? Time { get; set; } + } + + public class TimelinePatchRequest + { + public string? Description { get; set; } + + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs deleted file mode 100644 index 0649f1d1..00000000 --- a/Timeline/Models/Http/Token.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Timeline.Models.Http -{ - public class CreateTokenRequest - { - [Required] - public string Username { get; set; } = default!; - [Required] - public string Password { get; set; } = default!; - // in days, optional - [Range(1, 365)] - public int? Expire { get; set; } - } - - public class CreateTokenResponse - { - public string Token { get; set; } = default!; - public User User { get; set; } = default!; - } - - public class VerifyTokenRequest - { - [Required] - public string Token { get; set; } = default!; - } - - public class VerifyTokenResponse - { - public User User { get; set; } = default!; - } -} diff --git a/Timeline/Models/Http/TokenController.cs b/Timeline/Models/Http/TokenController.cs new file mode 100644 index 00000000..383b2965 --- /dev/null +++ b/Timeline/Models/Http/TokenController.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class CreateTokenRequest + { + [Required] + public string Username { get; set; } = default!; + [Required] + public string Password { get; set; } = default!; + // in days, optional + [Range(1, 365)] + public int? Expire { get; set; } + } + + public class CreateTokenResponse + { + public string Token { get; set; } = default!; + public UserInfoForAdmin User { get; set; } = default!; + } + + public class VerifyTokenRequest + { + [Required] + public string Token { get; set; } = default!; + } + + public class VerifyTokenResponse + { + public UserInfoForAdmin User { get; set; } = default!; + } +} diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs index 229ca1e5..e4c95cbd 100644 --- a/Timeline/Models/Http/UserController.cs +++ b/Timeline/Models/Http/UserController.cs @@ -1,5 +1,7 @@ +using AutoMapper; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; +using Timeline.Services; namespace Timeline.Models.Http { @@ -17,6 +19,21 @@ namespace Timeline.Models.Http public bool? Administrator { get; set; } } + public class CreateUserRequest + { + [Required, Username] + public string Username { get; set; } = default!; + + [Required, MinLength(1)] + public string Password { get; set; } = default!; + + [Required] + public bool? Administrator { get; set; } + + [Nickname] + public string? Nickname { get; set; } + } + public class ChangePasswordRequest { [Required(AllowEmptyStrings = false)] @@ -24,4 +41,13 @@ namespace Timeline.Models.Http [Required(AllowEmptyStrings = false)] public string NewPassword { get; set; } = default!; } + + public class UserControllerAutoMapperProfile : Profile + { + public UserControllerAutoMapperProfile() + { + CreateMap(MemberList.Source); + CreateMap(MemberList.Source); + } + } } diff --git a/Timeline/Models/Http/UserInfo.cs b/Timeline/Models/Http/UserInfo.cs new file mode 100644 index 00000000..6029b8aa --- /dev/null +++ b/Timeline/Models/Http/UserInfo.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Timeline.Controllers; +using Timeline.Services; + +namespace Timeline.Models.Http +{ + public interface IUserInfo + { + string Username { get; set; } + string Nickname { get; set; } + string AvatarUrl { get; set; } + } + + public class UserInfo : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + } + + public class UserInfoForAdmin : IUserInfo + { + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + public string AvatarUrl { get; set; } = default!; + public bool Administrator { get; set; } + } + + public class UserInfoSetAvatarUrlAction : IMappingAction + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public UserInfoSetAvatarUrlAction(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public void Process(object source, IUserInfo destination, ResolutionContext context) + { + var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); + destination.AvatarUrl = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController), new { destination.Username }); + } + } + + public class UserInfoAutoMapperProfile : Profile + { + public UserInfoAutoMapperProfile() + { + CreateMap().AfterMap(); + CreateMap().AfterMap(); + } + } +} diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs deleted file mode 100644 index 752c698d..00000000 --- a/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Timeline.Models -{ - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - - public class TimelinePostInfo - { - public long Id { get; set; } - - public string? Content { get; set; } - - public DateTime Time { get; set; } - - /// - /// The username of the author. - /// - public string Author { get; set; } = default!; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is a DTO class.")] - public class BaseTimelineInfo - { - public string? Description { get; set; } - - /// - /// The username of the owner. - /// - public string Owner { get; set; } = default!; - - public TimelineVisibility Visibility { get; set; } - - public List Members { get; set; } = default!; - } - - public class TimelineInfo : BaseTimelineInfo - { - public string Name { get; set; } = default!; - } -} diff --git a/Timeline/Models/Validation/NicknameValidator.cs b/Timeline/Models/Validation/NicknameValidator.cs index f6626a2a..53a2916b 100644 --- a/Timeline/Models/Validation/NicknameValidator.cs +++ b/Timeline/Models/Validation/NicknameValidator.cs @@ -5,7 +5,6 @@ namespace Timeline.Models.Validation { public class NicknameValidator : Validator { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base.")] protected override (bool, string) DoValidate(string value) { if (value.Length > 10) diff --git a/Timeline/Models/Validation/UsernameValidator.cs b/Timeline/Models/Validation/UsernameValidator.cs index fc6cdf37..d8f3bdc0 100644 --- a/Timeline/Models/Validation/UsernameValidator.cs +++ b/Timeline/Models/Validation/UsernameValidator.cs @@ -8,7 +8,6 @@ namespace Timeline.Models.Validation { public const int MaxLength = 26; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Already checked in base class.")] protected override (bool, string) DoValidate(string value) { if (value.Length == 0) diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs b/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs deleted file mode 100644 index e015c5fc..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Controllers.Testing { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class TestingI18nController { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal TestingI18nController() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.Testing.TestingI18nController", typeof(TestingI18nController).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to English test string.. - /// - internal static string TestString { - get { - return ResourceManager.GetString("TestString", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx b/Timeline/Resources/Controllers/Testing/TestingI18nController.resx deleted file mode 100644 index 57dfd5b9..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - English test string. - - \ No newline at end of file diff --git a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx b/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx deleted file mode 100644 index 6931cdf6..00000000 --- a/Timeline/Resources/Controllers/Testing/TestingI18nController.zh.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 中文测试字符串。 - - \ No newline at end of file diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 15101661..332c8817 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -168,6 +168,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to The user to set as member does not exist.. + /// + internal static string TimelineController_MemberPut_NotExist { + get { + return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to The post to delete does not exist.. /// @@ -267,15 +276,6 @@ namespace Timeline.Resources { } } - /// - /// Looks up a localized string similar to The new username already exists.. - /// - internal static string UserController_ChangeUsername_Conflict { - get { - return ResourceManager.GetString("UserController_ChangeUsername_Conflict", resourceCulture); - } - } - /// /// Looks up a localized string similar to You can't set permission unless you are administrator.. /// @@ -302,5 +302,14 @@ namespace Timeline.Resources { return ResourceManager.GetString("UserController_Patch_Forbid_Username", resourceCulture); } } + + /// + /// Looks up a localized string similar to A user with given username already exists.. + /// + internal static string UserController_UsernameConflict { + get { + return ResourceManager.GetString("UserController_UsernameConflict", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index db56ed02..cb6c3891 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -153,6 +153,9 @@ The {0}-st user to do operation {1} on does not exist. + + The user to set as member does not exist. + The post to delete does not exist. @@ -186,9 +189,6 @@ Old password is wrong. - - The new username already exists. - You can't set permission unless you are administrator. @@ -198,4 +198,7 @@ You can't set username unless you are administrator. + + A user with given username already exists. + \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index cada1788..e6806873 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -117,9 +117,9 @@ namespace Timeline.Resources.Services { /// /// Looks up a localized string similar to A present resource conflicts with the given resource.. /// - internal static string ConfictException { + internal static string ConflictException { get { - return ResourceManager.GetString("ConfictException", resourceCulture); + return ResourceManager.GetString("ConflictException", resourceCulture); } } @@ -258,42 +258,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The timeline with that name already exists.. - /// - internal static string TimelineAlreadyExistException { - get { - return ResourceManager.GetString("TimelineAlreadyExistException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An exception happened when add or remove member on timeline.. - /// - internal static string TimelineMemberOperationException { - get { - return ResourceManager.GetString("TimelineMemberOperationException", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An exception happened when do operation {0} on the {1} member on timeline.. - /// - internal static string TimelineMemberOperationExceptionDetail { - get { - return ResourceManager.GetString("TimelineMemberOperationExceptionDetail", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException.. - /// - internal static string TimelineNameBadFormatException { - get { - return ResourceManager.GetString("TimelineNameBadFormatException", resourceCulture); - } - } - /// /// Looks up a localized string similar to Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException.. /// @@ -312,15 +276,6 @@ namespace Timeline.Resources.Services { } } - /// - /// Looks up a localized string similar to The use is not a member of the timeline.. - /// - internal static string TimelineUserNotMemberException { - get { - return ResourceManager.GetString("TimelineUserNotMemberException", resourceCulture); - } - } - /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 2cb0f11a..11ae5f27 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -135,7 +135,7 @@ The password is wrong. - + A present resource conflicts with the given resource. @@ -183,27 +183,12 @@ Password is of bad format. - - The timeline with that name already exists. - - - An exception happened when add or remove member on timeline. - - - An exception happened when do operation {0} on the {1} member on timeline. - - - Timeline name is of bad format. If this is a personal timeline, it means the username is of bad format and inner exception should be a UsernameBadFormatException. - Timeline does not exist. If this is a personal timeline, it means the user does not exist and inner exception should be a UserNotExistException. The timeline post does not exist. You can't do operation on it. - - The use is not a member of the timeline. - The user does not exist. diff --git a/Timeline/Resources/Services/TimelineService.Designer.cs b/Timeline/Resources/Services/TimelineService.Designer.cs new file mode 100644 index 00000000..8212c252 --- /dev/null +++ b/Timeline/Resources/Services/TimelineService.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TimelineService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TimelineService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.TimelineService", typeof(TimelineService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The number {0} username is invalid.. + /// + internal static string ExceptionChangeMemberUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionChangeMemberUsernameBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The owner username of personal timeline is of bad format.. + /// + internal static string ExceptionFindTimelineUsernameBadFormat { + get { + return ResourceManager.GetString("ExceptionFindTimelineUsernameBadFormat", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/TimelineService.resx b/Timeline/Resources/Services/TimelineService.resx new file mode 100644 index 00000000..0429a2f8 --- /dev/null +++ b/Timeline/Resources/Services/TimelineService.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The number {0} username is invalid. + + + The owner username of personal timeline is of bad format. + + \ No newline at end of file diff --git a/Timeline/Resources/Services/UserCache.Designer.cs b/Timeline/Resources/Services/UserCache.Designer.cs deleted file mode 100644 index 28a74a6c..00000000 --- a/Timeline/Resources/Services/UserCache.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserCache { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserCache() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserCache", typeof(UserCache).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Found user info from cache. Entry: {0} .. - /// - internal static string LogGetCacheExist { - get { - return ResourceManager.GetString("LogGetCacheExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info not exist in cache. Id: {0} .. - /// - internal static string LogGetCacheNotExist { - get { - return ResourceManager.GetString("LogGetCacheNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info remove in cache. Id: {0} .. - /// - internal static string LogRemoveCache { - get { - return ResourceManager.GetString("LogRemoveCache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User info set in cache. Entry: {0} .. - /// - internal static string LogSetCache { - get { - return ResourceManager.GetString("LogSetCache", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserCache.resx b/Timeline/Resources/Services/UserCache.resx deleted file mode 100644 index 1102108b..00000000 --- a/Timeline/Resources/Services/UserCache.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Found user info from cache. Entry: {0} . - - - User info not exist in cache. Id: {0} . - - - User info remove in cache. Id: {0} . - - - User info set in cache. Entry: {0} . - - \ No newline at end of file diff --git a/Timeline/Resources/Services/UserDetailService.Designer.cs b/Timeline/Resources/Services/UserDetailService.Designer.cs deleted file mode 100644 index 2f586b36..00000000 --- a/Timeline/Resources/Services/UserDetailService.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Resources.Services { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class UserDetailService { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal UserDetailService() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserDetailService", typeof(UserDetailService).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Length of nickname can't be bigger than 10.. - /// - internal static string ExceptionNicknameTooLong { - get { - return ResourceManager.GetString("ExceptionNicknameTooLong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user_details entity has been created. User id is {0}. Nickname is {1}.. - /// - internal static string LogEntityNicknameCreate { - get { - return ResourceManager.GetString("LogEntityNicknameCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}.. - /// - internal static string LogEntityNicknameSetNotNull { - get { - return ResourceManager.GetString("LogEntityNicknameSetNotNull", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Nickname of a user_details entity has been updated to null. User id is {0}.. - /// - internal static string LogEntityNicknameSetToNull { - get { - return ResourceManager.GetString("LogEntityNicknameSetToNull", resourceCulture); - } - } - } -} diff --git a/Timeline/Resources/Services/UserDetailService.resx b/Timeline/Resources/Services/UserDetailService.resx deleted file mode 100644 index ea32aeda..00000000 --- a/Timeline/Resources/Services/UserDetailService.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Length of nickname can't be bigger than 10. - - - A user_details entity has been created. User id is {0}. Nickname is {1}. - - - Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}. - - - Nickname of a user_details entity has been updated to null. User id is {0}. - - \ No newline at end of file diff --git a/Timeline/Services/ConfictException.cs b/Timeline/Services/ConfictException.cs deleted file mode 100644 index dcd77366..00000000 --- a/Timeline/Services/ConfictException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services -{ - /// - /// Thrown when a resource already exists and conflicts with the given resource. - /// - /// - /// For example a username already exists and conflicts with the given username. - /// - [Serializable] - public class ConfictException : Exception - { - public ConfictException() : base(Resources.Services.Exception.ConfictException) { } - public ConfictException(string message) : base(message) { } - public ConfictException(string message, Exception inner) : base(message, inner) { } - protected ConfictException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/Timeline/Services/ConflictException.cs b/Timeline/Services/ConflictException.cs new file mode 100644 index 00000000..6ede183a --- /dev/null +++ b/Timeline/Services/ConflictException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services +{ + /// + /// Thrown when a resource already exists and conflicts with the given resource. + /// + /// + /// For example a username already exists and conflicts with the given username. + /// + [Serializable] + public class ConflictException : Exception + { + public ConflictException() : base(Resources.Services.Exception.ConflictException) { } + public ConflictException(string message) : base(message) { } + public ConflictException(string message, Exception inner) : base(message, inner) { } + protected ConflictException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs deleted file mode 100644 index e77dd01a..00000000 --- a/Timeline/Services/DatabaseExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Models.Validation; - -namespace Timeline.Services -{ - internal static class DatabaseExtensions - { - private static readonly UsernameValidator usernameValidator = new UsernameValidator(); - - /// - /// Check the existence and get the id of the user. - /// - /// The username of the user. - /// The user id. - /// Thrown if is null. - /// Thrown if is of bad format. - /// Thrown if user does not exist. - internal static async Task CheckAndGetUser(DbSet userDbSet, string? username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - var (result, message) = usernameValidator.Validate(username); - if (!result) - throw new UsernameBadFormatException(username, message); - - var userId = await userDbSet.Where(u => u.Username == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); - return userId; - } - } -} diff --git a/Timeline/Services/TimelineAlreadyExistException.cs b/Timeline/Services/TimelineAlreadyExistException.cs deleted file mode 100644 index c2dea1f9..00000000 --- a/Timeline/Services/TimelineAlreadyExistException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineAlreadyExistException : Exception - { - public TimelineAlreadyExistException() : base(Resources.Services.Exception.TimelineAlreadyExistException) { } - public TimelineAlreadyExistException(string name) : base(Resources.Services.Exception.TimelineAlreadyExistException) { Name = name; } - public TimelineAlreadyExistException(string name, Exception inner) : base(Resources.Services.Exception.TimelineAlreadyExistException, inner) { Name = name; } - protected TimelineAlreadyExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? Name { get; set; } - } -} diff --git a/Timeline/Services/TimelineMemberOperationUserException.cs b/Timeline/Services/TimelineMemberOperationUserException.cs deleted file mode 100644 index 543ee160..00000000 --- a/Timeline/Services/TimelineMemberOperationUserException.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Globalization; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineMemberOperationUserException : Exception - { - public enum MemberOperation - { - Add, - Remove - } - - public TimelineMemberOperationUserException() : base(Resources.Services.Exception.TimelineMemberOperationException) { } - public TimelineMemberOperationUserException(string message) : base(message) { } - public TimelineMemberOperationUserException(string message, Exception inner) : base(message, inner) { } - protected TimelineMemberOperationUserException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public TimelineMemberOperationUserException(int index, MemberOperation operation, string username, Exception inner) - : base(MakeMessage(operation, index), inner) { Operation = operation; Index = index; Username = username; } - - private static string MakeMessage(MemberOperation operation, int index) => string.Format(CultureInfo.CurrentCulture, - Resources.Services.Exception.TimelineMemberOperationExceptionDetail, operation, index); - - public MemberOperation? Operation { get; set; } - - /// - /// The index of the member on which the operation failed. - /// - public int? Index { get; set; } - - public string? Username { get; set; } - } -} diff --git a/Timeline/Services/TimelineNameBadFormatException.cs b/Timeline/Services/TimelineNameBadFormatException.cs deleted file mode 100644 index 5120a175..00000000 --- a/Timeline/Services/TimelineNameBadFormatException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services -{ - [Serializable] - public class TimelineNameBadFormatException : Exception - { - public TimelineNameBadFormatException() - : base(Resources.Services.Exception.TimelineNameBadFormatException) { } - public TimelineNameBadFormatException(string name) - : base(Resources.Services.Exception.TimelineNameBadFormatException) { Name = name; } - public TimelineNameBadFormatException(string name, Exception inner) - : base(Resources.Services.Exception.TimelineNameBadFormatException, inner) { Name = name; } - - protected TimelineNameBadFormatException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public string? Name { get; set; } - } -} diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index f43d2de5..89936aa2 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -1,13 +1,15 @@ -using Microsoft.EntityFrameworkCore; +using AutoMapper; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; +using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { @@ -28,12 +30,7 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// A list of all posts. /// Thrown when is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -46,26 +43,20 @@ namespace Timeline.Services /// Create a new post in timeline. /// /// Username or the timeline name. See remarks of . - /// The author's username. + /// The author's id. /// The content. /// The time of the post. If null, then use current time. /// The info of the created post. - /// Thrown when or or is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when or is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// Thrown if is of bad format. - /// Thrown if does not exist. - Task CreatePost(string name, string author, string content, DateTime? time); + /// Thrown if user with does not exist. + Task CreatePost(string name, long authorId, string content, DateTime? time); /// /// Delete a post @@ -73,12 +64,7 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// The id of the post to delete. /// Thrown when or is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -100,19 +86,14 @@ namespace Timeline.Services /// Username or the timeline name. See remarks of . /// The new properties. Null member means not to change. /// Thrown when or is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties); + Task ChangeProperty(string name, TimelinePatchRequest newProperties); /// /// Remove members to a timeline. @@ -121,24 +102,16 @@ namespace Timeline.Services /// A list of usernames of members to add. May be null. /// A list of usernames of members to remove. May be null. /// Thrown when is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). + /// Thrown when names in or is not a valid username. /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// - /// Thrown when an exception occurs on the user list. - /// The inner exception is - /// when one of the username is invalid. - /// The inner exception is - /// when one of the user to change does not exist. + /// + /// Thrown when one of the user to change does not exist. /// /// /// Operating on a username that is of bad format or does not exist always throws. @@ -153,42 +126,30 @@ namespace Timeline.Services /// Verify whether a visitor has the permission to read a timeline. /// /// Username or the timeline name. See remarks of . - /// The user to check on. Null means visitor without account. + /// The id of the user to check on. Null means visitor without account. /// True if can read, false if can't read. /// Thrown when is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// - /// Thrown when is of bad format. - /// - /// - /// Thrown when does not exist. - /// - Task HasReadPermission(string name, string? username); + /// + /// This method does not check whether visitor is administrator. + /// Return false if user with visitor id does not exist. + /// + Task HasReadPermission(string name, long? visitorId); /// /// Verify whether a user has the permission to modify a post. /// /// Username or the timeline name. See remarks of . - /// The user to check on. + /// The id of the user to check on. /// True if can modify, false if can't modify. - /// Thrown when or is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -198,47 +159,32 @@ namespace Timeline.Services /// /// Thrown when the post with given id does not exist or is deleted already. /// - /// - /// Thrown when is of bad format. - /// - /// - /// Thrown when does not exist. - /// /// /// This method does not check whether the user is administrator. /// It only checks whether he is the author of the post or the owner of the timeline. + /// Return false when user with modifier id does not exist. /// - Task HasPostModifyPermission(string name, long id, string username); + Task HasPostModifyPermission(string name, long id, long modifierId); /// /// Verify whether a user is member of a timeline. /// /// Username or the timeline name. See remarks of . - /// The user to check on. + /// The id of user to check on. /// True if it is a member, false if not. - /// Thrown when or is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is null. + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. /// For personal timeline, it means the user of that username does not exist /// and the inner exception should be a . /// - /// - /// Thrown when is not a valid username. - /// - /// - /// Thrown when user does not exist. - /// /// /// Timeline owner is also considered as a member. + /// Return false when user with user id does not exist. /// - Task IsMemberOf(string name, string username); + Task IsMemberOf(string name, long userId); } /// @@ -252,7 +198,7 @@ namespace Timeline.Services /// The name of the timeline. /// The timeline info. /// Thrown when is null. - /// + /// /// Thrown when timeline name is invalid. Currently it means it is an empty string. /// /// @@ -264,20 +210,12 @@ namespace Timeline.Services /// Create a timeline. /// /// The name of the timeline. - /// The owner of the timeline. + /// The id of owner of the timeline. /// Thrown when or is null. - /// - /// Thrown when timeline name is invalid. Currently it means it is an empty string. - /// - /// - /// Thrown when the timeline already exists. - /// - /// - /// Thrown when the username of the owner is not valid. - /// - /// - /// Thrown when the owner user does not exist. - Task CreateTimeline(string name, string owner); + /// Thrown when timeline name is invalid. Currently it means it is an empty string. + /// Thrown when the timeline already exists. + /// Thrown when the owner user does not exist. + Task CreateTimeline(string name, long owner); } public interface IPersonalTimelineService : IBaseTimelineService @@ -290,8 +228,8 @@ namespace Timeline.Services /// /// Thrown when is null. /// - /// - /// Thrown when is of bad format. Inner exception MUST be . + /// + /// Thrown when is of bad format. /// /// /// Thrown when the user does not exist. Inner exception MUST be . @@ -301,10 +239,12 @@ namespace Timeline.Services public abstract class BaseTimelineService : IBaseTimelineService { - protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) + protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock) { Clock = clock; Database = database; + UserService = userService; + Mapper = mapper; } protected IClock Clock { get; } @@ -313,6 +253,10 @@ namespace Timeline.Services protected DatabaseContext Database { get; } + protected IUserService UserService { get; } + + protected IMapper Mapper { get; } + /// /// Find the timeline id by the name. /// For details, see remarks. @@ -320,12 +264,7 @@ namespace Timeline.Services /// The username or the timeline name. See remarks. /// The id of the timeline entity. /// Thrown when is null. - /// - /// Thrown when timeline name is of bad format. - /// For normal timeline, it means name is an empty string. - /// For personal timeline, it means the username is of bad format, - /// the inner exception should be a . - /// + /// Thrown when is illegal. It is not a valid timeline name (for normal timeline service) or a valid username (for personal timeline service). /// /// Thrown when timeline does not exist. /// For normal timeline, it means the name does not exist. @@ -347,66 +286,60 @@ namespace Timeline.Services if (name == null) throw new ArgumentNullException(nameof(name)); + var timelineId = await FindTimelineId(name); var postEntities = await Database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId && p.Content != null).ToListAsync(); + var posts = new List(); foreach (var entity in postEntities) { - posts.Add(new TimelinePostInfo + if (entity.Content != null) // otherwise it is deleted { - Id = entity.Id, - Content = entity.Content, - Author = (await Database.Users.Where(u => u.Id == entity.AuthorId).Select(u => new { u.Username }).SingleAsync()).Name, - Time = entity.Time - }); + var author = Mapper.Map(UserService.GetUserById(entity.AuthorId)); + posts.Add(new TimelinePostInfo + { + Id = entity.Id, + Content = entity.Content, + Author = author, + Time = entity.Time, + LastUpdated = entity.LastUpdated + }); + } } return posts; } - public async Task CreatePost(string name, string author, string content, DateTime? time) + public async Task CreatePost(string name, long authorId, string content, DateTime? time) { if (name == null) throw new ArgumentNullException(nameof(name)); - if (author == null) - throw new ArgumentNullException(nameof(author)); if (content == null) throw new ArgumentNullException(nameof(content)); - { - var (result, message) = UsernameValidator.Validate(author); - if (!result) - { - throw new UsernameBadFormatException(author, message); - } - } - var timelineId = await FindTimelineId(name); - - var authorEntity = Database.Users.Where(u => u.Username == author).Select(u => new { u.Id }).SingleOrDefault(); - if (authorEntity == null) - { - throw new UserNotExistException(author); - } - var authorId = authorEntity.Id; + var author = Mapper.Map(await UserService.GetUserById(authorId)); var currentTime = Clock.GetCurrentTime(); + var finalTime = time ?? currentTime; var postEntity = new TimelinePostEntity { Content = content, AuthorId = authorId, TimelineId = timelineId, - Time = time ?? currentTime, + Time = finalTime, LastUpdated = currentTime }; - Database.TimelinePosts.Add(postEntity); await Database.SaveChangesAsync(); - return new TimelinePostCreateResponse + return new TimelinePostInfo { Id = postEntity.Id, - Time = postEntity.Time + Content = content, + Author = author, + Time = finalTime, + LastUpdated = currentTime }; } @@ -426,7 +359,7 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } - public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties) + public async Task ChangeProperty(string name, TimelinePatchRequest newProperties) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -455,27 +388,23 @@ namespace Timeline.Services if (name == null) throw new ArgumentNullException(nameof(name)); - // remove duplication and check the format of each username. - // Return a username->index map. - Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation) + List? RemoveDuplicateAndCheckFormat(IList? list, string paramName) { if (list != null) { - Dictionary result = new Dictionary(); + List result = new List(); var count = list.Count; for (var index = 0; index < count; index++) { var username = list[index]; - if (result.ContainsKey(username)) + if (result.Contains(username)) { continue; } var (validationResult, message) = UsernameValidator.Validate(username); if (!validationResult) - throw new TimelineMemberOperationUserException( - index, operation, username, - new UsernameBadFormatException(username, message)); - result.Add(username, index); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName)); + result.Add(username); } return result; } @@ -484,13 +413,13 @@ namespace Timeline.Services return null; } } - var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add); - var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove); + var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add)); + var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove)); // remove those both in add and remove if (simplifiedAdd != null && simplifiedRemove != null) { - var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u)); + var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList(); foreach (var u in usersToClean) { simplifiedAdd.Remove(u); @@ -500,26 +429,20 @@ namespace Timeline.Services var timelineId = await FindTimelineId(name); - async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation) + async Task?> CheckExistenceAndGetId(List? list) { - if (map == null) + if (list == null) return null; List result = new List(); - foreach (var (username, index) in map) + foreach (var username in list) { - var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - if (user == null) - { - throw new TimelineMemberOperationUserException(index, operation, username, - new UserNotExistException(username)); - } - result.Add(user.Id); + result.Add(await UserService.GetUserIdByUsername(username)); } return result; } - var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add); - var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove); + var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd); + var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove); if (userIdsAdd != null) { @@ -536,30 +459,11 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } - public async Task HasReadPermission(string name, string? username) + public async Task HasReadPermission(string name, long? visitorId) { if (name == null) throw new ArgumentNullException(nameof(name)); - long? userId = null; - if (username != null) - { - var (result, message) = UsernameValidator.Validate(username); - if (!result) - { - throw new UsernameBadFormatException(username); - } - - var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (user == null) - { - throw new UserNotExistException(username); - } - - userId = user.Id; - } - var timelineId = await FindTimelineId(name); var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); @@ -567,43 +471,24 @@ namespace Timeline.Services if (timelineEntity.Visibility == TimelineVisibility.Public) return true; - if (timelineEntity.Visibility == TimelineVisibility.Register && username != null) + if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null) return true; - if (userId == null) + if (visitorId == null) { return false; } else { - var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync(); + var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync(); return memberEntity != null; } } - public async Task HasPostModifyPermission(string name, long id, string username) + public async Task HasPostModifyPermission(string name, long id, long modifierId) { if (name == null) throw new ArgumentNullException(nameof(name)); - if (username == null) - throw new ArgumentNullException(nameof(username)); - - { - var (result, message) = UsernameValidator.Validate(username); - if (!result) - { - throw new UsernameBadFormatException(username); - } - } - - var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (user == null) - { - throw new UserNotExistException(username); - } - - var userId = user.Id; var timelineId = await FindTimelineId(name); @@ -614,32 +499,13 @@ namespace Timeline.Services if (postEntity == null) throw new TimelinePostNotExistException(id); - return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId; + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; } - public async Task IsMemberOf(string name, string username) + public async Task IsMemberOf(string name, long userId) { if (name == null) throw new ArgumentNullException(nameof(name)); - if (username == null) - throw new ArgumentNullException(nameof(username)); - - { - var (result, message) = UsernameValidator.Validate(username); - if (!result) - { - throw new UsernameBadFormatException(username); - } - } - - var user = await Database.Users.Where(u => u.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (user == null) - { - throw new UserNotExistException(username); - } - - var userId = user.Id; var timelineId = await FindTimelineId(name); @@ -648,38 +514,33 @@ namespace Timeline.Services if (userId == timelineEntity.OwnerId) return true; - var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync(); - - return timelineMemberEntity != null; + return await Database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } } public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService { - public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock) - : base(loggerFactory, database, clock) + public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IUserService userService, IMapper mapper, IClock clock) + : base(loggerFactory, database, userService, mapper, clock) { } protected override async Task FindTimelineId(string name) { + long userId; + try { - var (result, message) = UsernameValidator.Validate(name); - if (!result) - { - throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message)); - } + userId = await UserService.GetUserIdByUsername(name); } - - var userEntity = await Database.Users.Where(u => u.Username == name).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (userEntity == null) + catch (ArgumentException e) { - throw new TimelineNotExistException(name, new UserNotExistException(name)); + throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(name), e); + } + catch (UserNotExistException e) + { + throw new TimelineNotExistException(name, e); } - - var userId = userEntity.Id; var timelineEntity = await Database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); @@ -715,16 +576,20 @@ namespace Timeline.Services var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync(); - var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Username).SingleAsync()).ToArray(); + var owner = Mapper.Map(await UserService.GetUserById(timelineEntity.OwnerId)); - var memberUsernames = await Task.WhenAll(memberUsernameTasks); + var members = new List(); + foreach (var memberEntity in timelineMemberEntities) + { + members.Add(Mapper.Map(await UserService.GetUserById(memberEntity.UserId))); + } return new BaseTimelineInfo { Description = timelineEntity.Description ?? "", - Owner = username, + Owner = owner, Visibility = timelineEntity.Visibility, - Members = memberUsernames.ToList() + Members = members }; } diff --git a/Timeline/Services/User.cs b/Timeline/Services/User.cs index f63a374e..09a472e5 100644 --- a/Timeline/Services/User.cs +++ b/Timeline/Services/User.cs @@ -1,14 +1,9 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using Timeline.Controllers; - -namespace Timeline.Services +namespace Timeline.Services { public class User { public string? Username { get; set; } public string? Nickname { get; set; } - public string? AvatarUrl { get; set; } #region adminsecret public bool? Administrator { get; set; } @@ -20,30 +15,4 @@ namespace Timeline.Services public long? Version { get; set; } #endregion secret } - - public static class UserExtensions - { - public static User EraseSecretAndFinalFill(this User user, IUrlHelper urlHelper, bool adminstrator) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - - var result = new User - { - Username = user.Username, - Nickname = user.Nickname, - AvatarUrl = urlHelper.ActionLink(action: nameof(UserAvatarController.Get), controller: nameof(UserAvatarController), values: new - { - user.Username - }) - }; - - if (adminstrator) - { - result.Administrator = user.Administrator; - } - - return result; - } - } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ac7dd857..39b408e6 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -11,7 +11,6 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models.Validation; namespace Timeline.Services { @@ -61,36 +60,27 @@ namespace Timeline.Services public interface IUserAvatarService { /// - /// Get the etag of a user's avatar. + /// Get the etag of a user's avatar. Warning: This method does not check the user existence. /// - /// The username of the user to get avatar etag of. + /// The id of the user to get avatar etag of. /// The etag. - /// Thrown if is null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. - Task GetAvatarETag(string username); + Task GetAvatarETag(long id); /// - /// Get avatar of a user. If the user has no avatar set, a default one is returned. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. Warning: This method does not check the user existence. /// - /// The username of the user to get avatar of. + /// The id of the user to get avatar of. /// The avatar info. - /// Thrown if is null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. - Task GetAvatar(string username); + Task GetAvatar(long id); /// - /// Set avatar for a user. + /// Set avatar for a user. Warning: This method does not check the user existence. /// - /// The username of the user to set avatar for. + /// The id of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. - /// Throw if is null. /// Thrown if any field in is null when is not null. - /// Thrown if the is of bad format. - /// Thrown if the user does not exist. /// Thrown if avatar is of bad format. - Task SetAvatar(string username, Avatar? avatar); + Task SetAvatar(long id, Avatar? avatar); } // TODO! : Make this configurable. @@ -104,7 +94,6 @@ namespace Timeline.Services private DateTime _cacheLastModified; private string _cacheETag = default!; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")] public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); @@ -195,22 +184,18 @@ namespace Timeline.Services _clock = clock; } - public async Task GetAvatarETag(string username) + public async Task GetAvatarETag(long id) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else return eTag; } - public async Task GetAvatar(string username) + public async Task GetAvatar(long id) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); if (avatarEntity != null) { @@ -240,7 +225,7 @@ namespace Timeline.Services return defaultAvatar; } - public async Task SetAvatar(string username, Avatar? avatar) + public async Task SetAvatar(long id, Avatar? avatar) { if (avatar != null) { @@ -250,8 +235,7 @@ namespace Timeline.Services throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).SingleOrDefaultAsync(); if (avatar == null) { @@ -281,7 +265,7 @@ namespace Timeline.Services avatarEntity.Data = avatar.Data; avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = _clock.GetCurrentTime(); - avatarEntity.UserId = userId; + avatarEntity.UserId = id; if (create) { _database.UserAvatars.Add(avatarEntity); diff --git a/Timeline/Services/UserRoleConvert.cs b/Timeline/Services/UserRoleConvert.cs index 4fa4a7b8..f27ee1bb 100644 --- a/Timeline/Services/UserRoleConvert.cs +++ b/Timeline/Services/UserRoleConvert.cs @@ -5,7 +5,6 @@ using Timeline.Entities; namespace Timeline.Services { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")] public static class UserRoleConvert { public const string UserRole = UserRoles.User; diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ff2306c5..1197bb73 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -43,6 +43,16 @@ namespace Timeline.Services /// Thrown when the user with given username does not exist. Task GetUserByUsername(string username); + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsername(string username); + /// /// List all users. /// @@ -57,7 +67,7 @@ namespace Timeline.Services /// The id of the new user. /// Thrown when is null. /// Thrown when some fields in is bad. - /// Thrown when a user with given username already exists. + /// Thrown when a user with given username already exists. /// /// must not be null and must be a valid username. /// must not be null or empty. @@ -78,13 +88,12 @@ namespace Timeline.Services /// Only , , and will be used. /// If null, then not change. /// Other fields are ignored. - /// After modified, even if nothing is changed, version will increase. + /// Version will increase if password is changed. /// /// must be a valid username if set. /// can't be empty if set. /// must be a valid nickname if set. /// - /// Note: Whether is set or not, version will increase and not set to the specified value if there is one. /// /// Task ModifyUser(long id, User? info); @@ -97,6 +106,7 @@ namespace Timeline.Services /// Thrown when is null. /// Thrown when is of bad format or some fields in is bad. /// Thrown when user with given id does not exist. + /// Thrown when user with the newusername already exist. /// /// Only , and will be used. /// If null, then not change. @@ -184,7 +194,7 @@ namespace Timeline.Services private static void ThrowUsernameConflict() { - throw new ConfictException(ExceptionUsernameConflict); + throw new ConflictException(ExceptionUsernameConflict); } private static User CreateUserFromEntity(UserEntity entity) @@ -245,6 +255,21 @@ namespace Timeline.Services return CreateUserFromEntity(entity); } + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + CheckUsernameFormat(username, nameof(username)); + + var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + public async Task GetUsers() { var entities = await _databaseContext.Users.ToArrayAsync(); @@ -325,6 +350,7 @@ namespace Timeline.Services if (password != null) { entity.Password = _passwordService.HashPassword(password); + entity.Version += 1; } var administrator = info.Administrator; @@ -339,8 +365,6 @@ namespace Timeline.Services entity.Nickname = nickname; } } - - entity.Version += 1; } diff --git a/Timeline/Services/UserTokenService.cs b/Timeline/Services/UserTokenService.cs index c246fdff..cf7286f4 100644 --- a/Timeline/Services/UserTokenService.cs +++ b/Timeline/Services/UserTokenService.cs @@ -49,7 +49,6 @@ namespace Timeline.Services private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); private SymmetricSecurityKey _tokenSecurityKey; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "")] public JwtUserTokenService(IOptionsMonitor jwtConfig, IClock clock) { _jwtConfig = jwtConfig; diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 091a16e5..998b5c44 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; @@ -47,7 +48,6 @@ namespace Timeline }); services.Configure(Configuration.GetSection(nameof(JwtConfig))); - var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); services.AddAuthentication(AuthenticationConstants.Scheme) .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); @@ -75,16 +75,14 @@ namespace Timeline }); } - services.AddLocalization(options => - { - options.ResourcesPath = "Resources"; - }); + services.AddAutoMapper(GetType().Assembly); + + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddTransient(); - services.AddTransient(); services.AddUserAvatarService(); services.AddScoped(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 82b45094..25d73068 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -15,6 +15,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,14 +28,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + @@ -42,11 +44,6 @@ True AuthHandler.resx - - True - True - TestingI18nController.resx - True True @@ -73,9 +70,9 @@ Filters.resx - True - True - Messages.resx + True + True + Messages.resx True @@ -83,9 +80,9 @@ Common.resx - True - True - NicknameValidator.resx + True + True + NicknameValidator.resx True @@ -102,20 +99,15 @@ True Exception.resx - - True - True - UserAvatarService.resx - - + True True - UserCache.resx + TimelineService.resx - + True True - UserDetailService.resx + UserAvatarService.resx True @@ -129,10 +121,6 @@ ResXFileCodeGenerator AuthHandler.Designer.cs - - ResXFileCodeGenerator - TestingI18nController.Designer.cs - ResXFileCodeGenerator TimelineController.Designer.cs @@ -155,16 +143,16 @@ Filters.Designer.cs - ResXFileCodeGenerator - Messages.Designer.cs + ResXFileCodeGenerator + Messages.Designer.cs ResXFileCodeGenerator Common.Designer.cs - ResXFileCodeGenerator - NicknameValidator.Designer.cs + ResXFileCodeGenerator + NicknameValidator.Designer.cs ResXFileCodeGenerator @@ -178,17 +166,13 @@ ResXFileCodeGenerator Exception.Designer.cs - - ResXFileCodeGenerator - UserAvatarService.Designer.cs - - + ResXFileCodeGenerator - UserCache.Designer.cs + TimelineService.Designer.cs - + ResXFileCodeGenerator - UserDetailService.Designer.cs + UserAvatarService.Designer.cs ResXFileCodeGenerator -- cgit v1.2.3