From 415a91f08422a9a18958552ec21a25f336ef81c4 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 31 Jan 2021 15:42:52 +0800 Subject: ... --- .../IntegratedTests/TimelinePostTest.cs | 28 +++ .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 99 +------- BackEnd/Timeline/Controllers/TimelineController.cs | 278 ++------------------- .../Timeline/Controllers/TimelinePostController.cs | 213 ++++++++++++++++ BackEnd/Timeline/Models/Http/TimelineController.cs | 25 +- BackEnd/Timeline/Models/Mapper/TimelineMapper.cs | 4 +- BackEnd/Timeline/Properties/launchSettings.json | 14 +- BackEnd/Timeline/Services/TimelineService.cs | 99 ++------ 8 files changed, 308 insertions(+), 452 deletions(-) create mode 100644 BackEnd/Timeline/Controllers/TimelinePostController.cs diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs index 0060ac04..ae7afda1 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelinePostTest.cs @@ -122,6 +122,34 @@ namespace Timeline.Tests.IntegratedTests } } + [Theory] + [MemberData(nameof(TimelineNameGeneratorTestData))] + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + posts.Add(post); + await Task.Delay(1000); + } + + await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); + + { + + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); + posts.Select(p => p.Content == null).Should().Equal(false, true, false); + } + } + [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] public async Task PostList_IncludeDeleted(TimelineNameGenerator generator) diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs index 4247e572..28fcb9fa 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1,8 +1,6 @@ using FluentAssertions; using System; using System.Collections.Generic; -using System.Globalization; -using System.Linq; using System.Net; using System.Threading.Tasks; using Timeline.Models; @@ -11,7 +9,6 @@ using Xunit; namespace Timeline.Tests.IntegratedTests { - public class TimelineTest : BaseTimelineTest { [Fact] @@ -22,7 +19,6 @@ namespace Timeline.Tests.IntegratedTests await client.TestGetAssertInvalidModelAsync("timelines/@!!!"); await client.TestGetAssertInvalidModelAsync("timelines/!!!"); - { var body = await client.TestGetAsync("timelines/@user1"); body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1")); @@ -343,90 +339,6 @@ namespace Timeline.Tests.IntegratedTests } } - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - - var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); - posts.Add(post); - await Task.Delay(1000); - } - - await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}"); - - { - - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); - posts.Should().HaveCount(3); - posts.Select(p => p.Deleted).Should().Equal(false, true, false); - posts.Select(p => p.Content == null).Should().Equal(false, true, false); - } - } - - [Theory] - [MemberData(nameof(TimelineNameGeneratorTestData))] - public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineNameGenerator generator) - { - using var client = await CreateClientAsUser(); - - DateTime lastModifiedTime; - HttpTimeline timeline; - string uniqueId; - - { - var body = await client.GetTimelineAsync(generator(1)); - timeline = body; - lastModifiedTime = body.LastModified; - uniqueId = body.UniqueId; - } - - { - await client.TestGetAsync($"timelines/{generator(1)}", - expectedStatusCode: HttpStatusCode.NotModified, - headerSetup: (headers, _) => - { - headers.IfModifiedSince = lastModifiedTime.AddSeconds(1); - }); - } - - { - - var body = await client.TestGetAsync($"timelines/{generator(1)}", - headerSetup: (headers, _) => - { - headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); - }); - body.Should().BeEquivalentTo(timeline); - } - - { - await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }", expectedStatusCode: HttpStatusCode.NotModified); - } - - { - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }"); - body.Should().BeEquivalentTo(timeline); - } - - { - await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={uniqueId}", expectedStatusCode: HttpStatusCode.NotModified); - } - - { - var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}"); - body.Should().BeEquivalentTo(timeline); - } - } - [Theory] [MemberData(nameof(TimelineNameGeneratorTestData))] public async Task Title(TimelineNameGenerator generator) @@ -454,21 +366,20 @@ namespace Timeline.Tests.IntegratedTests { { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPatchAssertUnauthorizedAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "tttttttt" }); } { using var client = await CreateClientAs(2); - await client.TestPostAssertForbiddenAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPatchAssertForbiddenAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "tttttttt" }); } using (var client = await CreateClientAsUser()) { - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); - await client.TestPostAssertErrorAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist); + await client.TestPatchAssertInvalidModelAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "!!!" }); + await client.TestPatchAssertErrorAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "t2" }, errorCode: ErrorCodes.TimelineController.NameConflict); - await client.TestPostAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); + await client.TestPatchAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "newt" }); await client.TestGetAsync("timelines/t1", expectedStatusCode: HttpStatusCode.NotFound); diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 5d484388..06ab8004 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -6,9 +6,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Timeline.Entities; using Timeline.Filters; -using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Mapper; @@ -22,13 +20,13 @@ namespace Timeline.Controllers /// Operations about timeline. /// [ApiController] + [Route("timelines")] [CatchTimelineNotExistException] [ProducesErrorResponseType(typeof(CommonResponse))] public class TimelineController : Controller { private readonly IUserService _userService; private readonly ITimelineService _service; - private readonly ITimelinePostService _postService; private readonly TimelineMapper _timelineMapper; private readonly IMapper _mapper; @@ -36,11 +34,10 @@ namespace Timeline.Controllers /// /// /// - public TimelineController(IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, TimelineMapper timelineMapper, IMapper mapper) + public TimelineController(IUserService userService, ITimelineService service, TimelineMapper timelineMapper, IMapper mapper) { _userService = userService; _service = service; - _postService = timelinePostService; _timelineMapper = timelineMapper; _mapper = mapper; } @@ -54,7 +51,7 @@ namespace Timeline.Controllers /// Specify the relation type, may be 'own' or 'join'. If not set, both type will return. /// "Private" or "Register" or "Public". If set, only timelines whose visibility is specified one will return. /// The timeline list. - [HttpGet("timelines")] + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) @@ -117,254 +114,50 @@ namespace Timeline.Controllers /// Get info of a timeline. /// /// The timeline name. - /// A unique id. If specified and if-modified-since is also specified, the timeline info will return when unique id is not the specified one even if it is not modified. - /// Same effect as If-Modified-Since header and take precedence than it. - /// If specified, will return 304 if not modified. /// The timeline info. - [HttpGet("timelines/{timeline}")] + [HttpGet("{timeline}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> TimelineGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) + public async Task> TimelineGet([FromRoute][GeneralTimelineName] string timeline) { - DateTime? ifModifiedSince = null; - if (queryIfModifiedSince.HasValue) - { - ifModifiedSince = queryIfModifiedSince.Value; - } - else if (headerIfModifiedSince is not null) - { - ifModifiedSince = headerIfModifiedSince.Value; - } - var timelineId = await _service.GetTimelineIdByName(timeline); - - bool returnNotModified = false; - - if (ifModifiedSince.HasValue) - { - var lastModified = await _service.GetTimelineLastModifiedTime(timelineId); - if (lastModified < ifModifiedSince.Value) - { - if (checkUniqueId != null) - { - var uniqueId = await _service.GetTimelineUniqueId(timelineId); - if (uniqueId == checkUniqueId) - { - returnNotModified = true; - } - } - else - { - returnNotModified = true; - } - } - } - - if (returnNotModified) - { - return StatusCode(StatusCodes.Status304NotModified); - } - else - { - var t = await _service.GetTimeline(timelineId); - var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId()); - return result; - } - } - - /// - /// Get posts of a timeline. - /// - /// The name of the timeline. - /// If set, only posts modified since the time will return. - /// If set to true, deleted post will also return. - /// The post list. - [HttpGet("timelines/{timeline}/posts")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> PostListGet([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) - { - var timelineId = await _service.GetTimelineIdByName(timeline); - - if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false); - - var result = await _timelineMapper.MapToHttp(posts, timeline, Url); + var t = await _service.GetTimeline(timelineId); + var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId()); return result; } /// - /// Get the data of a post. Usually a image post. - /// - /// Timeline name. - /// The id of the post. - /// If-None-Match header. - /// The data. - [HttpGet("timelines/{timeline}/posts/{post}/data")] - [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] - [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) - { - _ = ifNoneMatch; - - var timelineId = await _service.GetTimelineIdByName(timeline); - - if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(timelineId, this.GetOptionalUserId())) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - return await DataCacheHelper.GenerateActionResult(this, - () => _postService.GetPostDataETag(timelineId, post), - async () => await _postService.GetPostData(timelineId, post)); - } - catch (TimelinePostNotExistException) - { - return NotFound(ErrorResponse.TimelineController.PostNotExist()); - } - catch (TimelinePostNoDataException) - { - return BadRequest(ErrorResponse.TimelineController.PostNoData()); - } - } - - /// - /// Create a new post. + /// Change properties of a timeline. /// /// Timeline name. /// - /// Info of new post. - [HttpPost("timelines/{timeline}/posts")] + /// The new info. + [HttpPatch("{timeline}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) + public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body) { var timelineId = await _service.GetTimelineIdByName(timeline); - var userId = this.GetUserId(); - if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(timelineId, userId)) + if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermission(timelineId, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - var content = body.Content; - - TimelinePostEntity post; - - if (content.Type == TimelinePostContentTypes.Text) - { - var text = content.Text; - if (text == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); - } - post = await _postService.CreateTextPost(timelineId, userId, text, body.Time); - } - else if (content.Type == TimelinePostContentTypes.Image) - { - var base64Data = content.Data; - if (base64Data == null) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); - } - byte[] data; - try - { - data = Convert.FromBase64String(base64Data); - } - catch (FormatException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); - } - - try - { - post = await _postService.CreateImagePost(timelineId, userId, data, body.Time); - } - catch (ImageException) - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); - } - } - else - { - return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); - } - - var result = await _timelineMapper.MapToHttp(post, timeline, Url); - return result; - } - - /// - /// Delete a post. - /// - /// Timeline name. - /// Post id. - /// Info of deletion. - [HttpDelete("timelines/{timeline}/posts/{post}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) - { - var timelineId = await _service.GetTimelineIdByName(timeline); - try { - if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - await _postService.DeletePost(timelineId, post); - return Ok(); - } - catch (TimelinePostNotExistException) - { - return BadRequest(ErrorResponse.TimelineController.PostNotExist()); + await _service.ChangeProperty(timelineId, _mapper.Map(body)); + var t = await _service.GetTimeline(timelineId); + var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId()); + return result; } - } - - /// - /// Change properties of a timeline. - /// - /// Timeline name. - /// - /// The new info. - [HttpPatch("timelines/{timeline}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body) - { - var timelineId = await _service.GetTimelineIdByName(timeline); - - if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermission(timelineId, this.GetUserId())) + catch (EntityAlreadyExistException) { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + return BadRequest(ErrorResponse.TimelineController.NameConflict()); } - await _service.ChangeProperty(timelineId, _mapper.Map(body)); - var t = await _service.GetTimeline(timelineId); - var result = await _timelineMapper.MapToHttp(t, Url, this.GetOptionalUserId()); - return result; } /// @@ -372,7 +165,7 @@ namespace Timeline.Controllers /// /// Timeline name. /// The new member's username. - [HttpPut("timelines/{timeline}/members/{member}")] + [HttpPut("{timeline}/members/{member}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -404,7 +197,7 @@ namespace Timeline.Controllers /// /// Timeline name. /// The member's username. - [HttpDelete("timelines/{timeline}/members/{member}")] + [HttpDelete("{timeline}/members/{member}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -436,7 +229,7 @@ namespace Timeline.Controllers /// /// /// Info of new timeline. - [HttpPost("timelines")] + [HttpPost] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -462,7 +255,7 @@ namespace Timeline.Controllers /// /// Timeline name. /// Info of deletion. - [HttpDelete("timelines/{timeline}")] + [HttpDelete("{timeline}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -487,32 +280,5 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.TimelineController.NotExist()); } } - - [HttpPost("timelineop/changename")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body) - { - var timelineId = await _service.GetTimelineIdByName(body.OldName); - - if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(timelineId, this.GetUserId()))) - { - return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); - } - - try - { - await _service.ChangeTimelineName(timelineId, body.NewName); - var timeline = await _service.GetTimeline(timelineId); - return await _timelineMapper.MapToHttp(timeline, Url, this.GetOptionalUserId()); - } - catch (EntityAlreadyExistException) - { - return BadRequest(ErrorResponse.TimelineController.NameConflict()); - } - } } } diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs new file mode 100644 index 00000000..afe9b36f --- /dev/null +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -0,0 +1,213 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Filters; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Mapper; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Operations about timeline. + /// + [ApiController] + [Route("timelines/{timeline}/posts")] + [CatchTimelineNotExistException] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class TimelinePostController : Controller + { + private readonly ITimelineService _timelineService; + private readonly ITimelinePostService _postService; + + private readonly TimelineMapper _timelineMapper; + + /// + /// + /// + public TimelinePostController(ITimelineService timelineService, ITimelinePostService timelinePostService, TimelineMapper timelineMapper) + { + _timelineService = timelineService; + _postService = timelinePostService; + _timelineMapper = timelineMapper; + } + + private bool UserHasAllTimelineManagementPermission => this.UserHasPermission(UserPermission.AllTimelineManagement); + + /// + /// Get posts of a timeline. + /// + /// The name of the timeline. + /// If set, only posts modified since the time will return. + /// If set to true, deleted post will also return. + /// The post list. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> PostList([FromRoute][GeneralTimelineName] string timeline, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var posts = await _postService.GetPosts(timelineId, modifiedSince, includeDeleted ?? false); + + var result = await _timelineMapper.MapToHttp(posts, timeline, Url); + return result; + } + + /// + /// Get the data of a post. Usually a image post. + /// + /// Timeline name. + /// The id of the post. + /// If-None-Match header. + /// The data. + [HttpGet("{post}/data")] + [Produces("image/png", "image/jpeg", "image/gif", "image/webp", "application/json", "text/json")] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task PostDataGet([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + { + _ = ifNoneMatch; + + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermission(timelineId, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + return await DataCacheHelper.GenerateActionResult(this, + () => _postService.GetPostDataETag(timelineId, post), + async () => await _postService.GetPostData(timelineId, post)); + } + catch (TimelinePostNotExistException) + { + return NotFound(ErrorResponse.TimelineController.PostNotExist()); + } + catch (TimelinePostNoDataException) + { + return BadRequest(ErrorResponse.TimelineController.PostNoData()); + } + } + + /// + /// Create a new post. + /// + /// Timeline name. + /// + /// Info of new post. + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> PostPost([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) + { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + var userId = this.GetUserId(); + + if (!UserHasAllTimelineManagementPermission && !await _timelineService.IsMemberOf(timelineId, userId)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + var content = body.Content; + + TimelinePostEntity post; + + if (content.Type == TimelinePostContentTypes.Text) + { + var text = content.Text; + if (text == null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); + } + post = await _postService.CreateTextPost(timelineId, userId, text, body.Time); + } + else if (content.Type == TimelinePostContentTypes.Image) + { + var base64Data = content.Data; + if (base64Data == null) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataRequired)); + } + byte[] data; + try + { + data = Convert.FromBase64String(base64Data); + } + catch (FormatException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotBase64)); + } + + try + { + post = await _postService.CreateImagePost(timelineId, userId, data, body.Time); + } + catch (ImageException) + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ImageContentDataNotImage)); + } + } + else + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); + } + + var result = await _timelineMapper.MapToHttp(post, timeline, Url); + return result; + } + + /// + /// Delete a post. + /// + /// Timeline name. + /// Post id. + /// Info of deletion. + [HttpDelete("{post}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task PostDelete([FromRoute][GeneralTimelineName] string timeline, [FromRoute] long post) + { + var timelineId = await _timelineService.GetTimelineIdByName(timeline); + + try + { + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(timelineId, post, this.GetUserId(), true)) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + await _postService.DeletePost(timelineId, post); + return Ok(); + } + catch (TimelinePostNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.PostNotExist()); + } + } + } +} diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs index 257076f0..79be1826 100644 --- a/BackEnd/Timeline/Models/Http/TimelineController.cs +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -58,6 +58,12 @@ namespace Timeline.Models.Http /// public class HttpTimelinePatchRequest { + /// + /// New name. Null for not change. + /// + [TimelineName] + public string? Name { get; set; } + /// /// New title. Null for not change. /// @@ -74,25 +80,6 @@ namespace Timeline.Models.Http public TimelineVisibility? Visibility { get; set; } } - /// - /// Change timeline name request model. - /// - public class HttpTimelineChangeNameRequest - { - /// - /// Old name of timeline. - /// - [Required] - [TimelineName] - public string OldName { get; set; } = default!; - /// - /// New name of timeline. - /// - [Required] - [TimelineName] - public string NewName { get; set; } = default!; - } - public class HttpTimelineControllerAutoMapperProfile : Profile { public HttpTimelineControllerAutoMapperProfile() diff --git a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs index 95418573..79a6fa1d 100644 --- a/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs +++ b/BackEnd/Timeline/Models/Mapper/TimelineMapper.cs @@ -48,7 +48,7 @@ namespace Timeline.Models.Mapper isBookmark: userId is not null && await _bookmarkTimelineService.IsBookmark(userId.Value, entity.Id, false, false), links: new HttpTimelineLinks( self: urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName }), - posts: urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName }) + posts: urlHelper.ActionLink(nameof(TimelinePostController.PostList), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName }) ) ); } @@ -83,7 +83,7 @@ namespace Timeline.Models.Mapper ( type: TimelinePostContentTypes.Image, text: null, - url: urlHelper.ActionLink(nameof(TimelineController.PostDataGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }), + url: urlHelper.ActionLink(nameof(TimelinePostController.PostDataGet), nameof(TimelinePostController)[0..^nameof(Controller).Length], new { timeline = timelineName, post = entity.LocalId }), eTag: $"\"{entity.Content}\"" ), _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, "Unknown timeline post type {0}.", entity.ContentType)) diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json index db58cd31..851fc6a8 100644 --- a/BackEnd/Timeline/Properties/launchSettings.json +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -2,11 +2,11 @@ "profiles": { "Dev": { "commandName": "Project", - "applicationUrl": "http://0.0.0.0:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_FRONTEND": "Proxy" - } + }, + "applicationUrl": "http://0.0.0.0:5000" }, "Dev-Mock": { "commandName": "Project", @@ -20,6 +20,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Staging" } + }, + "Dev-Windows": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_FRONTEND": "Proxy", + "TIMELINE_WORKDIR": "D:\\timeline-development" + } } } -} +} \ No newline at end of file diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index 1d1bb320..f4141752 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -49,6 +49,7 @@ namespace Timeline.Services public class TimelineChangePropertyParams { + public string? Name { get; set; } public string? Title { get; set; } public string? Description { get; set; } public TimelineVisibility? Visibility { get; set; } @@ -59,22 +60,6 @@ namespace Timeline.Services /// public interface ITimelineService : IBasicTimelineService { - /// - /// Get the timeline last modified time (not include name change). - /// - /// The id of the timeline. - /// The timeline modified time. - /// Thrown when timeline does not exist. - Task GetTimelineLastModifiedTime(long id); - - /// - /// Get the timeline unique id. - /// - /// The id of the timeline. - /// The timeline unique id. - /// Thrown when timeline does not exist. - Task GetTimelineUniqueId(long id); - /// /// Get the timeline info. /// @@ -90,6 +75,7 @@ namespace Timeline.Services /// The new properties. Null member means not to change. /// Thrown when is null. /// Thrown when timeline with given id does not exist. + /// Thrown when a timeline with new name already exists. Task ChangeProperty(long id, TimelineChangePropertyParams newProperties); /// @@ -180,20 +166,6 @@ namespace Timeline.Services /// The id of the timeline to delete. /// Thrown when the timeline does not exist. Task DeleteTimeline(long id); - - /// - /// Change name of a timeline. - /// - /// The timeline id. - /// The new timeline name. - /// Thrown when is null. - /// Thrown when is of invalid format. - /// Thrown when timeline does not exist. - /// Thrown when a timeline with new name already exists. - /// - /// You can only change name of general timeline. - /// - Task ChangeTimelineName(long id, string newTimelineName); } public class TimelineService : BasicTimelineService, ITimelineService @@ -222,26 +194,6 @@ namespace Timeline.Services } } - public async Task GetTimelineLastModifiedTime(long id) - { - var entity = await _database.Timelines.Where(t => t.Id == id).Select(t => new { t.LastModified }).SingleOrDefaultAsync(); - - if (entity is null) - throw new TimelineNotExistException(id); - - return entity.LastModified; - } - - public async Task GetTimelineUniqueId(long id) - { - var entity = await _database.Timelines.Where(t => t.Id == id).Select(t => new { t.UniqueId }).SingleOrDefaultAsync(); - - if (entity is null) - throw new TimelineNotExistException(id); - - return entity.UniqueId; - } - public async Task GetTimeline(long id) { var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync(); @@ -257,12 +209,29 @@ namespace Timeline.Services if (newProperties is null) throw new ArgumentNullException(nameof(newProperties)); + if (newProperties.Name is not null) + ValidateTimelineName(newProperties.Name, nameof(newProperties)); + var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync(); if (entity is null) throw new TimelineNotExistException(id); var changed = false; + var nameChanged = false; + + if (newProperties.Name is not null) + { + var conflict = await _database.Timelines.AnyAsync(t => t.Name == newProperties.Name); + + if (conflict) + throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); + + entity.Name = newProperties.Name; + + changed = true; + nameChanged = true; + } if (newProperties.Title != null) { @@ -286,6 +255,8 @@ namespace Timeline.Services { var currentTime = _clock.GetCurrentTime(); entity.LastModified = currentTime; + if (nameChanged) + entity.NameLastModified = currentTime; } await _database.SaveChangesAsync(); @@ -447,34 +418,6 @@ namespace Timeline.Services _database.Timelines.Remove(entity); await _database.SaveChangesAsync(); } - - public async Task ChangeTimelineName(long id, string newTimelineName) - { - if (newTimelineName == null) - throw new ArgumentNullException(nameof(newTimelineName)); - - ValidateTimelineName(newTimelineName, nameof(newTimelineName)); - - var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync(); - - if (entity is null) - throw new TimelineNotExistException(id); - - if (entity.Name == newTimelineName) return; - - var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName); - - if (conflict) - throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - - var now = _clock.GetCurrentTime(); - - entity.Name = newTimelineName; - entity.NameLastModified = now; - entity.LastModified = now; - - await _database.SaveChangesAsync(); - } } public static class TimelineServiceExtensions -- cgit v1.2.3