From 9e84b1e9ad1f2a45cd3e09759c69989fdc588c3d Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 2 Feb 2020 00:31:33 +0800 Subject: ... --- Timeline/Models/Http/TimelineCommon.cs | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) (limited to 'Timeline/Models/Http') diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs index febb8186..0b2a714c 100644 --- a/Timeline/Models/Http/TimelineCommon.cs +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -1,5 +1,7 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; using System.Collections.Generic; +using Timeline.Controllers; namespace Timeline.Models.Http { @@ -28,17 +30,41 @@ namespace Timeline.Models.Http 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 class TimelineInfo { + public string? Name { get; set; } public string Description { get; set; } = default!; public UserInfo Owner { get; set; } = default!; public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + +#pragma warning disable CA1707 // Identifiers should not contain underscores + public TimelineInfoLinks? _links { get; set; } +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + public class TimelineInfoLinks + { + public string Posts { get; set; } = default!; } - public class TimelineInfo : BaseTimelineInfo + public static class TimelineInfoExtensions { - public string Name { get; set; } = default!; + public static TimelineInfo FillLinks(this TimelineInfo info, IUrlHelper urlHelper) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + if (urlHelper == null) + throw new ArgumentNullException(nameof(urlHelper)); + + info._links = new TimelineInfoLinks + { + Posts = urlHelper.ActionLink(nameof(PersonalTimelineController.PostListGet), nameof(PersonalTimelineController)[0..^nameof(Controller).Length], new { info.Owner.Username }) + }; + + return info; + } } } -- cgit v1.2.3 From 12f85448cde94d70d9030b757b09caa5e2f53061 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 2 Feb 2020 22:37:47 +0800 Subject: ... --- Timeline/Controllers/PersonalTimelineController.cs | 10 +- Timeline/Controllers/TimelineController.cs | 131 +++++++++++++++++++++ Timeline/Models/Http/TimelineCommon.cs | 17 ++- Timeline/Services/TimelineService.cs | 31 +++++ 4 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 Timeline/Controllers/TimelineController.cs (limited to 'Timeline/Models/Http') diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 842da015..8cf098bf 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -28,7 +28,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}/timeline")] public async Task> TimelineGet([FromRoute][Username] string username) { - return (await _service.GetTimeline(username)).FillLinks(Url); + return (await _service.GetTimeline(username)).FillLinksForPersonalTimeline(Url); } [HttpGet("users/{username}/timeline/posts")] @@ -79,12 +79,12 @@ namespace Timeline.Controllers [Authorize] public async Task> TimelinePatch([FromRoute][Username] string username, [FromBody] TimelinePatchRequest body) { - if (!this.IsAdministrator() && !(User.Identity.Name == username)) + if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } await _service.ChangeProperty(username, body); - var timeline = (await _service.GetTimeline(username)).FillLinks(Url); + var timeline = (await _service.GetTimeline(username)).FillLinksForPersonalTimeline(Url); return Ok(timeline); } @@ -92,7 +92,7 @@ namespace Timeline.Controllers [Authorize] public async Task TimelineMemberPut([FromRoute][Username] string username, [FromRoute][Username] string member) { - if (!this.IsAdministrator() && !(User.Identity.Name == username)) + if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } @@ -112,7 +112,7 @@ namespace Timeline.Controllers [Authorize] public async Task TimelineMemberDelete([FromRoute][Username] string username, [FromRoute][Username] string member) { - if (!this.IsAdministrator() && !(User.Identity.Name == username)) + if (!this.IsAdministrator() && !(await _service.HasManagePermission(username, this.GetUserId()))) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs new file mode 100644 index 00000000..be271de7 --- /dev/null +++ b/Timeline/Controllers/TimelineController.cs @@ -0,0 +1,131 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; + +namespace Timeline.Controllers +{ + [ApiController] + public class TimelineController : Controller + { + private readonly ILogger _logger; + + private readonly ITimelineService _service; + + public TimelineController(ILogger logger, ITimelineService service) + { + _logger = logger; + _service = service; + } + + [HttpGet("timelines/{name}")] + public async Task> TimelineGet([FromRoute][TimelineName] string name) + { + return (await _service.GetTimeline(name)).FillLinksForNormalTimeline(Url); + } + + [HttpGet("timelines/{name}/posts")] + public async Task>> PostListGet([FromRoute][TimelineName] string name) + { + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + return await _service.GetPosts(name); + } + + [HttpPost("timelines/{name}/posts")] + [Authorize] + public async Task> PostPost([FromRoute][TimelineName] string name, [FromBody] TimelinePostCreateRequest body) + { + var id = this.GetUserId(); + if (!this.IsAdministrator() && !await _service.IsMemberOf(name, id)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var res = await _service.CreatePost(name, id, body.Content, body.Time); + return res; + } + + [HttpDelete("timelines/{name}/posts/{id}")] + [Authorize] + public async Task PostDelete([FromRoute][TimelineName] string name, [FromRoute] long id) + { + try + { + if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + await _service.DeletePost(name, id); + return Ok(CommonDeleteResponse.Delete()); + } + catch (TimelinePostNotExistException) + { + return Ok(CommonDeleteResponse.NotExist()); + } + } + + [HttpPatch("timelines/{name}")] + [Authorize] + public async Task> TimelinePatch([FromRoute][TimelineName] string name, [FromBody] TimelinePatchRequest body) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + await _service.ChangeProperty(name, body); + var timeline = (await _service.GetTimeline(name)).FillLinksForNormalTimeline(Url); + return Ok(timeline); + } + + [HttpPut("timelines/{name}/members/{member}")] + [Authorize] + public async Task TimelineMemberPut([FromRoute][TimelineName] string name, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.ChangeMember(name, new List { member }, null); + return Ok(); + } + catch (UserNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + } + } + + [HttpDelete("timelines/{name}/members/{member}")] + [Authorize] + public async Task TimelineMemberDelete([FromRoute][TimelineName] string name, [FromRoute][Username] string member) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.ChangeMember(name, null, new List { member }); + return Ok(CommonDeleteResponse.Delete()); + } + catch (UserNotExistException) + { + return Ok(CommonDeleteResponse.NotExist()); + } + } + + // TODO: Create API . + } +} diff --git a/Timeline/Models/Http/TimelineCommon.cs b/Timeline/Models/Http/TimelineCommon.cs index 0b2a714c..1cb47dac 100644 --- a/Timeline/Models/Http/TimelineCommon.cs +++ b/Timeline/Models/Http/TimelineCommon.cs @@ -52,7 +52,7 @@ namespace Timeline.Models.Http public static class TimelineInfoExtensions { - public static TimelineInfo FillLinks(this TimelineInfo info, IUrlHelper urlHelper) + public static TimelineInfo FillLinksForPersonalTimeline(this TimelineInfo info, IUrlHelper urlHelper) { if (info == null) throw new ArgumentNullException(nameof(info)); @@ -66,5 +66,20 @@ namespace Timeline.Models.Http return info; } + + public static TimelineInfo FillLinksForNormalTimeline(this TimelineInfo info, IUrlHelper urlHelper) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + if (urlHelper == null) + throw new ArgumentNullException(nameof(urlHelper)); + + info._links = new TimelineInfoLinks + { + Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { info.Name }) + }; + + return info; + } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index b031297e..991669ad 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -138,6 +138,26 @@ namespace Timeline.Services /// Task ChangeMember(string name, IList? add, IList? remove); + /// + /// Check whether a user can manage(change timeline info, member, ...) a timeline. + /// + /// + /// + /// True if the user can manage the timeline, otherwise false. + /// 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 . + /// + /// + /// This method does not check whether visitor is administrator. + /// Return false if user with user id does not exist. + /// + Task HasManagePermission(string name, long userId); + /// /// Verify whether a visitor has the permission to read a timeline. /// @@ -490,6 +510,17 @@ namespace Timeline.Services await Database.SaveChangesAsync(); } + public async Task HasManagePermission(string name, long userId) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = await FindTimelineId(name); + var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + return userId == timelineEntity.OwnerId; + } + public async Task HasReadPermission(string name, long? visitorId) { if (name == null) -- cgit v1.2.3 From bcf15408ca1a1a16ccaca959861e812e6b24a33d Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 2 Feb 2020 22:52:33 +0800 Subject: Add copy to clipboard function to code generator. --- ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj | 4 ++++ ErrorResponseCodeGenerator/Program.cs | 6 ++++++ Timeline/Models/Http/ErrorResponse.cs | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) (limited to 'Timeline/Models/Http') diff --git a/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj b/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj index e77a1ba3..8524b2e4 100644 --- a/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj +++ b/ErrorResponseCodeGenerator/ErrorResponseCodeGenerator.csproj @@ -5,6 +5,10 @@ netcoreapp3.1 + + + + diff --git a/ErrorResponseCodeGenerator/Program.cs b/ErrorResponseCodeGenerator/Program.cs index cf021927..5ea8aaca 100644 --- a/ErrorResponseCodeGenerator/Program.cs +++ b/ErrorResponseCodeGenerator/Program.cs @@ -57,6 +57,12 @@ $ ".Replace("$", code); Console.WriteLine(code); + + TextCopy.Clipboard.SetText(code); + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Code has copied to clipboard!"); + Console.ForegroundColor = oldColor; } } } diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index 87516638..a20b5386 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -1,4 +1,5 @@ -using static Timeline.Resources.Messages; + +using static Timeline.Resources.Messages; namespace Timeline.Models.Http { @@ -258,4 +259,4 @@ namespace Timeline.Models.Http } -} \ No newline at end of file +} -- cgit v1.2.3 From 5f93daed17338abbcade5eaba8936acbc4955b56 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 3 Feb 2020 17:50:17 +0800 Subject: Add TimelineController. --- Timeline.ErrorCodes/ErrorCodes.cs | 6 +++-- .../IntegratedTests/PersonalTimelineTest.cs | 2 +- Timeline/Controllers/PersonalTimelineController.cs | 2 +- Timeline/Controllers/TimelineController.cs | 19 +++++++++++++-- Timeline/Filters/Timeline.cs | 2 +- Timeline/Models/Http/ErrorResponse.cs | 26 ++++++++++++++++++--- Timeline/Models/Http/TimelineController.cs | 8 +++++++ Timeline/Resources/Messages.Designer.cs | 27 ++++++++-------------- Timeline/Resources/Messages.resx | 15 +++++------- 9 files changed, 70 insertions(+), 37 deletions(-) (limited to 'Timeline/Models/Http') diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs index ea74cf0e..e07fbd94 100644 --- a/Timeline.ErrorCodes/ErrorCodes.cs +++ b/Timeline.ErrorCodes/ErrorCodes.cs @@ -56,9 +56,11 @@ public const int BadFormat_BadSize = 1_103_00_03; } - public static class TimelineController + public static class TimelineCommon { - public const int MemberPut_NotExist = 1_104_01_01; + public const int NameConflict = 1_104_01_01; + public const int NotExist = 1_104_02_01; + public const int MemberPut_NotExist = 1_104_03_01; } } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index cc170a98..7d0a68e8 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -165,7 +165,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutAsync("/users/user1/timeline/members/usernotexist", null); res.Should().HaveStatusCode(400) - .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); + .And.HaveCommonBody(ErrorCodes.TimelineCommon.MemberPut_NotExist); } await AssertEmptyMembers(); { diff --git a/Timeline/Controllers/PersonalTimelineController.cs b/Timeline/Controllers/PersonalTimelineController.cs index 8cf098bf..b6c213d9 100644 --- a/Timeline/Controllers/PersonalTimelineController.cs +++ b/Timeline/Controllers/PersonalTimelineController.cs @@ -104,7 +104,7 @@ namespace Timeline.Controllers } catch (UserNotExistException) { - return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + return BadRequest(ErrorResponse.TimelineCommon.MemberPut_NotExist()); } } diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index be271de7..d46189b8 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -102,7 +102,7 @@ namespace Timeline.Controllers } catch (UserNotExistException) { - return BadRequest(ErrorResponse.TimelineController.MemberPut_NotExist()); + return BadRequest(ErrorResponse.TimelineCommon.MemberPut_NotExist()); } } @@ -126,6 +126,21 @@ namespace Timeline.Controllers } } - // TODO: Create API . + [HttpPost("timelines")] + [Authorize] + public async Task> TimelineCreate([FromRoute] TimelineCreateRequest body) + { + var userId = this.GetUserId(); + + try + { + var timelineInfo = await _service.CreateTimeline(body.Name, userId); + return Ok(timelineInfo); + } + catch (ConflictException) + { + return BadRequest(ErrorResponse.TimelineCommon.NameConflict()); + } + } } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index ed78e645..e133c9d6 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -17,7 +17,7 @@ namespace Timeline.Filters } else { - throw new System.NotImplementedException(); + context.Result = new NotFoundObjectResult(ErrorResponse.TimelineCommon.NotExist()); } } } diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index a20b5386..0d23fe59 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -242,17 +242,37 @@ namespace Timeline.Models.Http } - public static class TimelineController + public static class TimelineCommon { + public static CommonResponse NameConflict(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(TimelineCommon_NameConflict, formatArgs)); + } + + public static CommonResponse CustomMessage_NameConflict(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineCommon.NameConflict, string.Format(message, formatArgs)); + } + + public static CommonResponse NotExist(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineCommon.NotExist, string.Format(TimelineCommon_NotExist, formatArgs)); + } + + public static CommonResponse CustomMessage_NotExist(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.TimelineCommon.NotExist, string.Format(message, formatArgs)); + } + public static CommonResponse MemberPut_NotExist(params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(TimelineController_MemberPut_NotExist, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(TimelineCommon_MemberPut_NotExist, formatArgs)); } public static CommonResponse CustomMessage_MemberPut_NotExist(string message, params object?[] formatArgs) { - return new CommonResponse(ErrorCodes.TimelineController.MemberPut_NotExist, string.Format(message, formatArgs)); + return new CommonResponse(ErrorCodes.TimelineCommon.MemberPut_NotExist, string.Format(message, formatArgs)); } } diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs index f9a4d3e5..6d461bb9 100644 --- a/Timeline/Models/Http/TimelineController.cs +++ b/Timeline/Models/Http/TimelineController.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; namespace Timeline.Models.Http { @@ -11,6 +12,13 @@ namespace Timeline.Models.Http public DateTime? Time { get; set; } } + public class TimelineCreateRequest + { + [Required] + [TimelineName] + public string Name { get; set; } = default!; + } + public class TimelinePatchRequest { public string? Description { get; set; } diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 332c8817..eeb44f10 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -151,38 +151,29 @@ namespace Timeline.Resources { } /// - /// Looks up a localized string similar to The {0}-st username to do operation {1} on is of bad format.. - /// - internal static string TimelineController_ChangeMember_UsernameBadFormat { - get { - return ResourceManager.GetString("TimelineController_ChangeMember_UsernameBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The {0}-st user to do operation {1} on does not exist.. + /// Looks up a localized string similar to The user to set as member does not exist.. /// - internal static string TimelineController_ChangeMember_UserNotExist { + internal static string TimelineCommon_MemberPut_NotExist { get { - return ResourceManager.GetString("TimelineController_ChangeMember_UserNotExist", resourceCulture); + return ResourceManager.GetString("TimelineCommon_MemberPut_NotExist", resourceCulture); } } /// - /// Looks up a localized string similar to The user to set as member does not exist.. + /// Looks up a localized string similar to A timeline with given name already exists.. /// - internal static string TimelineController_MemberPut_NotExist { + internal static string TimelineCommon_NameConflict { get { - return ResourceManager.GetString("TimelineController_MemberPut_NotExist", resourceCulture); + return ResourceManager.GetString("TimelineCommon_NameConflict", resourceCulture); } } /// - /// Looks up a localized string similar to The post to delete does not exist.. + /// Looks up a localized string similar to The timeline with given name does not exist.. /// - internal static string TimelineController_PostOperationDelete_NotExist { + internal static string TimelineCommon_NotExist { get { - return ResourceManager.GetString("TimelineController_PostOperationDelete_NotExist", resourceCulture); + return ResourceManager.GetString("TimelineCommon_NotExist", resourceCulture); } } diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index cb6c3891..66a84d5f 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -147,17 +147,14 @@ Model is of bad format. - - The {0}-st username to do operation {1} on is of bad format. - - - 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. + + A timeline with given name already exists. + + + The timeline with given name does not exist. Username or password is invalid. -- cgit v1.2.3