From 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- BackEnd/Timeline/Controllers/TimelineController.cs | 491 +++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 BackEnd/Timeline/Controllers/TimelineController.cs (limited to 'BackEnd/Timeline/Controllers/TimelineController.cs') diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs new file mode 100644 index 00000000..9a3147ea --- /dev/null +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -0,0 +1,491 @@ +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Timeline.Filters; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Operations about timeline. + /// + [ApiController] + [CatchTimelineNotExistException] + [ProducesErrorResponseType(typeof(CommonResponse))] + public class TimelineController : Controller + { + private readonly ILogger _logger; + + private readonly IUserService _userService; + private readonly ITimelineService _service; + + private readonly IMapper _mapper; + + /// + /// + /// + public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper) + { + _logger = logger; + _userService = userService; + _service = service; + _mapper = mapper; + } + + /// + /// List all timelines. + /// + /// A username. If set, only timelines related to the user will return. + /// 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")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) + { + List? visibilityFilter = null; + if (visibility != null) + { + visibilityFilter = new List(); + var items = visibility.Split('|'); + foreach (var item in items) + { + if (item.Equals(nameof(TimelineVisibility.Private), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Private)) + visibilityFilter.Add(TimelineVisibility.Private); + } + else if (item.Equals(nameof(TimelineVisibility.Register), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Register)) + visibilityFilter.Add(TimelineVisibility.Register); + } + else if (item.Equals(nameof(TimelineVisibility.Public), StringComparison.OrdinalIgnoreCase)) + { + if (!visibilityFilter.Contains(TimelineVisibility.Public)) + visibilityFilter.Add(TimelineVisibility.Public); + } + else + { + return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_QueryVisibilityUnknown, item)); + } + } + } + + TimelineUserRelationship? relationship = null; + if (relate != null) + { + try + { + var relatedUserId = await _userService.GetUserIdByUsername(relate); + + relationship = new TimelineUserRelationship(relateType switch + { + "own" => TimelineUserRelationshipType.Own, + "join" => TimelineUserRelationshipType.Join, + _ => TimelineUserRelationshipType.Default + }, relatedUserId); + } + catch (UserNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.QueryRelateNotExist()); + } + } + + var timelines = await _service.GetTimelines(relationship, visibilityFilter); + var result = _mapper.Map>(timelines); + return result; + } + + /// + /// 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/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) + { + DateTime? ifModifiedSince = null; + if (queryIfModifiedSince.HasValue) + { + ifModifiedSince = queryIfModifiedSince.Value; + } + else if (headerIfModifiedSince != null) + { + ifModifiedSince = headerIfModifiedSince.Value; + } + + bool returnNotModified = false; + + if (ifModifiedSince.HasValue) + { + var lastModified = await _service.GetTimelineLastModifiedTime(name); + if (lastModified < ifModifiedSince.Value) + { + if (checkUniqueId != null) + { + var uniqueId = await _service.GetTimelineUniqueId(name); + if (uniqueId == checkUniqueId) + { + returnNotModified = true; + } + } + else + { + returnNotModified = true; + } + } + } + + if (returnNotModified) + { + return StatusCode(StatusCodes.Status304NotModified); + } + else + { + var timeline = await _service.GetTimeline(name); + var result = _mapper.Map(timeline); + 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/{name}/posts")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + { + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); + + var result = _mapper.Map>(posts); + 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/{name}/posts/{id}/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 name, [FromRoute] long id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) + { + _ = ifNoneMatch; + if (!this.IsAdministrator() && !await _service.HasReadPermission(name, this.GetOptionalUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => + { + var data = await _service.GetPostData(name, id); + return data; + }); + } + 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("timelines/{name}/posts")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> PostPost([FromRoute][GeneralTimelineName] 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 content = body.Content; + + TimelinePost 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 _service.CreateTextPost(name, id, 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 _service.CreateImagePost(name, id, 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 = _mapper.Map(post); + return result; + } + + /// + /// Delete a post. + /// + /// Timeline name. + /// Post id. + /// Info of deletion. + [HttpDelete("timelines/{name}/posts/{id}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) + { + if (!this.IsAdministrator() && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + try + { + await _service.DeletePost(name, id); + return CommonDeleteResponse.Delete(); + } + catch (TimelinePostNotExistException) + { + return CommonDeleteResponse.NotExist(); + } + } + + /// + /// Change properties of a timeline. + /// + /// Timeline name. + /// + /// The new info. + [HttpPatch("timelines/{name}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelinePatch([FromRoute][GeneralTimelineName] 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, _mapper.Map(body)); + var timeline = await _service.GetTimeline(name); + var result = _mapper.Map(timeline); + return result; + } + + /// + /// Add a member to timeline. + /// + /// Timeline name. + /// The new member's username. + [HttpPut("timelines/{name}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task TimelineMemberPut([FromRoute][GeneralTimelineName] 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()); + } + } + + /// + /// Remove a member from timeline. + /// + /// Timeline name. + /// The member's username. + [HttpDelete("timelines/{name}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task TimelineMemberDelete([FromRoute][GeneralTimelineName] 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()); + } + } + + /// + /// Create a timeline. + /// + /// + /// Info of new timeline. + [HttpPost("timelines")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) + { + var userId = this.GetUserId(); + + try + { + var timeline = await _service.CreateTimeline(body.Name, userId); + var result = _mapper.Map(timeline); + return result; + } + catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) + { + return BadRequest(ErrorResponse.TimelineController.NameConflict()); + } + } + + /// + /// Delete a timeline. + /// + /// Timeline name. + /// Info of deletion. + [HttpDelete("timelines/{name}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelineDelete([FromRoute][TimelineName] string name) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(name, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + await _service.DeleteTimeline(name); + return CommonDeleteResponse.Delete(); + } + catch (TimelineNotExistException) + { + return CommonDeleteResponse.NotExist(); + } + } + + [HttpPost("timelineop/changename")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); + return Ok(_mapper.Map(timeline)); + } + catch (EntityAlreadyExistException) + { + return BadRequest(ErrorResponse.TimelineController.NameConflict()); + } + } + } +} -- cgit v1.2.3