From bdcbe0612ae3e4e173754c5e663e2668e9f380ec Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 12 Apr 2022 18:07:17 +0800 Subject: ... --- .../Controllers/TimelineBookmarkV2Controller.cs | 177 ----------------- .../Controllers/TimelinePostV2Controller.cs | 218 --------------------- .../Timeline/Controllers/TimelineV2Controller.cs | 152 -------------- .../Controllers/V2/TimelineBookmarkV2Controller.cs | 177 +++++++++++++++++ .../Controllers/V2/TimelinePostV2Controller.cs | 218 +++++++++++++++++++++ .../Controllers/V2/TimelineV2Controller.cs | 152 ++++++++++++++ .../Timeline/Controllers/V2/UserV2Controller.cs | 181 +++++++++++++++++ .../Timeline/Controllers/V2/V2ControllerBase.cs | 28 +++ BackEnd/Timeline/Services/User/IUserService.cs | 5 +- BackEnd/Timeline/Services/User/UserService.cs | 15 +- BackEnd/Timeline/Timeline.csproj | 6 + 11 files changed, 780 insertions(+), 549 deletions(-) delete mode 100644 BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs delete mode 100644 BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs delete mode 100644 BackEnd/Timeline/Controllers/TimelineV2Controller.cs create mode 100644 BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs create mode 100644 BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs create mode 100644 BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs create mode 100644 BackEnd/Timeline/Controllers/V2/UserV2Controller.cs create mode 100644 BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs (limited to 'BackEnd') diff --git a/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs b/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs deleted file mode 100644 index 2b31f43e..00000000 --- a/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Services.Api; -using Timeline.Services.Timeline; -using Timeline.Services.User; - -namespace Timeline.Controllers -{ - [ApiController] - [Route("v2/users/{username}/bookmarks")] - public class TimelineBookmarkV2Controller : MyControllerBase - { - private readonly IUserService _userService; - private readonly ITimelineService _timelineService; - private readonly ITimelineBookmarkService1 _timelineBookmarkService; - - public TimelineBookmarkV2Controller(IUserService userService, ITimelineService timelineService, ITimelineBookmarkService1 timelineBookmarkService) - { - _userService = userService; - _timelineService = timelineService; - _timelineBookmarkService = timelineBookmarkService; - } - - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [HttpGet] - public async Task>> ListAsync([FromRoute][Username] string username, - [FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) - { - return Forbid(); - } - return await _timelineBookmarkService.GetBookmarksAsync(userId, page ?? 1, pageSize ?? 20); - } - - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [HttpGet("{index}")] - public async Task> GetAsync([FromRoute][Username] string username, [FromRoute][PositiveInteger] int index) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) - { - return Forbid(); - } - return await _timelineBookmarkService.GetBookmarkAtAsync(userId, index); - } - - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - [Authorize] - [HttpPost] - public async Task> CreateAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkCreateRequest body) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) - { - return Forbid(); - } - long timelineId; - try - { - timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); - } - catch (EntityNotExistException) - { - return UnprocessableEntity(); - } - var bookmark = await _timelineBookmarkService.AddBookmarkAsync(userId, timelineId, body.Position); - return CreatedAtAction("Get", new { username, index = bookmark.Position }, bookmark); - } - - [Authorize] - [HttpPost("delete")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task DeleteAsync([FromRoute][Username] string username, [FromBody] HttpTimelinebookmarkDeleteRequest body) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) - { - return Forbid(); - } - - long timelineId; - try - { - timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); - } - catch (EntityNotExistException) - { - return UnprocessableEntity(); - } - - await _timelineBookmarkService.DeleteBookmarkAsync(userId, timelineId); - - return NoContent(); - } - - [Authorize] - [HttpPost("move")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> MoveAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkMoveRequest body) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) - { - return Forbid(); - } - - long timelineId; - try - { - timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); - } - catch (EntityNotExistException) - { - return UnprocessableEntity(); - } - - var bookmark = await _timelineBookmarkService.MoveBookmarkAsync(userId, timelineId, body.Position!.Value); - - return Ok(bookmark); - } - - [HttpGet("visibility")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> GetVisibilityAsync([FromRoute][Username] string username) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - var visibility = await _timelineBookmarkService.GetBookmarkVisibilityAsync(userId); - return Ok(new HttpTimelineBookmarkVisibility { Visibility = visibility }); - } - - [HttpPut("visibility")] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task PutVisibilityAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkVisibility body) - { - var userId = await _userService.GetUserIdByUsernameAsync(username); - if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) - { - return Forbid(); - } - await _timelineBookmarkService.SetBookmarkVisibilityAsync(userId, body.Visibility); - return NoContent(); - } - } -} diff --git a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs b/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs deleted file mode 100644 index c80cda17..00000000 --- a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -using Timeline.Helpers.Cache; -using Timeline.Models; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services.Mapper; -using Timeline.Services.Timeline; -using Timeline.Services.User; -using Timeline.SignalRHub; - -namespace Timeline.Controllers -{ - [ApiController] - [Route("v2/timelines/{owner}/{timeline}/posts")] - public class TimelinePostV2Controller : MyControllerBase - { - private readonly ITimelineService _timelineService; - private readonly ITimelinePostService _postService; - - private readonly IGenericMapper _mapper; - - private readonly MarkdownProcessor _markdownProcessor; - - private readonly IHubContext _timelineHubContext; - - public TimelinePostV2Controller(ITimelineService timelineService, ITimelinePostService timelinePostService, IGenericMapper mapper, MarkdownProcessor markdownProcessor, IHubContext timelineHubContext) - { - _timelineService = timelineService; - _postService = timelinePostService; - _mapper = mapper; - _markdownProcessor = markdownProcessor; - _timelineHubContext = timelineHubContext; - } - - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task>> ListAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromQuery] DateTime? modifiedSince, - [FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) - { - return Forbid(); - } - var postPage = await _postService.GetPostsV2Async(timelineId, modifiedSince, page, pageSize); - var items = await _mapper.MapListAsync(postPage.Items, Url, User); - return postPage.WithItems(items); - } - - [HttpGet("{post}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status410Gone)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> GetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute(Name = "post")] long postId) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) - { - return Forbid(); - } - var post = await _postService.GetPostV2Async(timelineId, postId); - var result = await _mapper.MapAsync(post, Url, User); - return result; - } - - [HttpGet("{post}/data")] - [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] - [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status410Gone)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> DataIndexGetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post) - { - return await DataGetAsync(owner, timeline, post, 0); - } - - [HttpGet("{post}/data/{data_index}")] - [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] - [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status410Gone)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task DataGetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post, [FromRoute(Name = "data_index")][Range(0, 100)] long dataIndex) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) - { - return Forbid(); - } - - return await DataCacheHelper.GenerateActionResult(this, - () => _postService.GetPostDataDigestV2Async(timelineId, post, dataIndex), - async () => - { - var data = await _postService.GetPostDataV2Async(timelineId, post, dataIndex); - if (data.ContentType == MimeTypes.TextMarkdown) - { - return new ByteData(_markdownProcessor.Process(data.Data, Url, timeline, post), data.ContentType); - } - return data; - } - ); - } - - [HttpPost] - [Authorize] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> PostAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.IsMemberOfAsync(timelineId, GetAuthUserId())) - { - return Forbid(); - } - - var createRequest = new TimelinePostCreateRequest() - { - Time = body.Time, - Color = body.Color - }; - - for (int i = 0; i < body.DataList.Count; i++) - { - var data = body.DataList[i]; - - if (data is null) - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is null.")); - - try - { - var d = Convert.FromBase64String(data.Data); - createRequest.DataList.Add(new TimelinePostCreateRequestData(data.ContentType, d)); - } - catch (FormatException) - { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is not a valid base64 string.")); - } - } - - try - { - var post = await _postService.CreatePostAsync(timelineId, GetAuthUserId(), createRequest); - - var group = TimelineHub.GenerateTimelinePostChangeListeningGroupName(timeline); - await _timelineHubContext.Clients.Group(group).SendAsync(nameof(ITimelineClient.OnTimelinePostChanged), timeline); - - var result = await _mapper.MapAsync(post, Url, User); - return CreatedAtAction("Get", new { owner = owner, timeline = timeline, post = post.LocalId }, result); - } - catch (TimelinePostCreateDataException e) - { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {e.Index} is invalid. {e.Message}")); - } - } - - [HttpPatch("{post}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> Patch([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post, [FromBody] HttpTimelinePostPatchRequest body) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) - { - return Forbid(); - } - - var entity = await _postService.PatchPostAsync(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); - var result = await _mapper.MapAsync(entity, Url, User); - - return Ok(result); - } - - [HttpDelete("{post}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task Delete([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) - { - return Forbid(); - } - - await _postService.DeletePostAsync(timelineId, post); - - return NoContent(); - } - } -} - diff --git a/BackEnd/Timeline/Controllers/TimelineV2Controller.cs b/BackEnd/Timeline/Controllers/TimelineV2Controller.cs deleted file mode 100644 index 9811cbed..00000000 --- a/BackEnd/Timeline/Controllers/TimelineV2Controller.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Timeline.Entities; -using Timeline.Models.Http; -using Timeline.Models.Validation; -using Timeline.Services; -using Timeline.Services.Mapper; -using Timeline.Services.Timeline; -using Timeline.Services.User; - -namespace Timeline.Controllers -{ - [ApiController] - [Route("v2/timelines")] - public class TimelineV2Controller : MyControllerBase - { - private ITimelineService _timelineService; - private IGenericMapper _mapper; - private IUserService _userService; - - public TimelineV2Controller(ITimelineService timelineService, IGenericMapper mapper, IUserService userService) - { - _timelineService = timelineService; - _mapper = mapper; - _userService = userService; - } - - private Task MapAsync(TimelineEntity entity) - { - return _mapper.MapAsync(entity, Url, User); - } - - [HttpGet("{owner}/{timeline}")] - public async Task> GetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - var t = await _timelineService.GetTimelineAsync(timelineId); - return await MapAsync(t); - } - - [HttpPatch("{owner}/{timeline}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> PatchAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) - { - return Forbid(); - } - await _timelineService.ChangePropertyAsync(timelineId, _mapper.AutoMapperMap(body)); - var t = await _timelineService.GetTimelineAsync(timelineId); - return await MapAsync(t); - } - - [HttpDelete("{owner}/{timeline}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task DeleteAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) - { - return Forbid(); - } - await _timelineService.DeleteTimelineAsync(timelineId); - return NoContent(); - } - - [HttpPut("{owner}/{timeline}/members/{member}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task MemberPutAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute][Username] string member) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) - { - return Forbid(); - } - - long userId; - try - { - userId = await _userService.GetUserIdByUsernameAsync(member); - } - catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) - { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); - } - await _timelineService.AddMemberAsync(timelineId, userId); - return NoContent(); - } - - [HttpDelete("{owner}/{timeline}/members/{member}")] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task MemberDeleteAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute][Username] string member) - { - var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); - if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) - { - return Forbid(); - } - - long userId; - try - { - userId = await _userService.GetUserIdByUsernameAsync(member); - } - catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) - { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); - } - await _timelineService.RemoveMemberAsync(timelineId, userId); - return NoContent(); - } - - [HttpPost] - [Authorize] - [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] - public async Task> TimelineCreate([FromBody] HttpTimelineCreateRequest body) - { - var authUserId = GetAuthUserId(); - var authUser = await _userService.GetUserAsync(authUserId); - var timeline = await _timelineService.CreateTimelineAsync(authUserId, body.Name); - var result = await MapAsync(timeline); - return CreatedAtAction("Get", new { owner = authUser.Username, timeline = body.Name }, result); - } - } -} - diff --git a/BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs new file mode 100644 index 00000000..a23a061b --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs @@ -0,0 +1,177 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Api; +using Timeline.Services.Timeline; +using Timeline.Services.User; + +namespace Timeline.Controllers.V2 +{ + [ApiController] + [Route("v2/users/{username}/bookmarks")] + public class TimelineBookmarkV2Controller : V2ControllerBase + { + private readonly IUserService _userService; + private readonly ITimelineService _timelineService; + private readonly ITimelineBookmarkService1 _timelineBookmarkService; + + public TimelineBookmarkV2Controller(IUserService userService, ITimelineService timelineService, ITimelineBookmarkService1 timelineBookmarkService) + { + _userService = userService; + _timelineService = timelineService; + _timelineBookmarkService = timelineBookmarkService; + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [HttpGet] + public async Task>> ListAsync([FromRoute][Username] string username, + [FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) + { + return Forbid(); + } + return await _timelineBookmarkService.GetBookmarksAsync(userId, page ?? 1, pageSize ?? 20); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [HttpGet("{index}")] + public async Task> GetAsync([FromRoute][Username] string username, [FromRoute][PositiveInteger] int index) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && !await _timelineBookmarkService.CanReadBookmarksAsync(userId, GetOptionalAuthUserId())) + { + return Forbid(); + } + return await _timelineBookmarkService.GetBookmarkAtAsync(userId, index); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [Authorize] + [HttpPost] + public async Task> CreateAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkCreateRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + var bookmark = await _timelineBookmarkService.AddBookmarkAsync(userId, timelineId, body.Position); + return CreatedAtAction("Get", new { username, index = bookmark.Position }, bookmark); + } + + [Authorize] + [HttpPost("delete")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task DeleteAsync([FromRoute][Username] string username, [FromBody] HttpTimelinebookmarkDeleteRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + + await _timelineBookmarkService.DeleteBookmarkAsync(userId, timelineId); + + return NoContent(); + } + + [Authorize] + [HttpPost("move")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> MoveAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkMoveRequest body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + + long timelineId; + try + { + timelineId = await _timelineService.GetTimelineIdAsync(body.TimelineOwner, body.TimelineName); + } + catch (EntityNotExistException) + { + return UnprocessableEntity(); + } + + var bookmark = await _timelineBookmarkService.MoveBookmarkAsync(userId, timelineId, body.Position!.Value); + + return Ok(bookmark); + } + + [HttpGet("visibility")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> GetVisibilityAsync([FromRoute][Username] string username) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + var visibility = await _timelineBookmarkService.GetBookmarkVisibilityAsync(userId); + return Ok(new HttpTimelineBookmarkVisibility { Visibility = visibility }); + } + + [HttpPut("visibility")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task PutVisibilityAsync([FromRoute][Username] string username, [FromBody] HttpTimelineBookmarkVisibility body) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (!UserHasPermission(UserPermission.UserBookmarkManagement) && GetAuthUserId() != userId) + { + return Forbid(); + } + await _timelineBookmarkService.SetBookmarkVisibilityAsync(userId, body.Visibility); + return NoContent(); + } + } +} diff --git a/BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs new file mode 100644 index 00000000..aa839abf --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs @@ -0,0 +1,218 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Timeline.Helpers.Cache; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services.Mapper; +using Timeline.Services.Timeline; +using Timeline.Services.User; +using Timeline.SignalRHub; + +namespace Timeline.Controllers.V2 +{ + [ApiController] + [Route("v2/timelines/{owner}/{timeline}/posts")] + public class TimelinePostV2Controller : V2ControllerBase + { + private readonly ITimelineService _timelineService; + private readonly ITimelinePostService _postService; + + private readonly IGenericMapper _mapper; + + private readonly MarkdownProcessor _markdownProcessor; + + private readonly IHubContext _timelineHubContext; + + public TimelinePostV2Controller(ITimelineService timelineService, ITimelinePostService timelinePostService, IGenericMapper mapper, MarkdownProcessor markdownProcessor, IHubContext timelineHubContext) + { + _timelineService = timelineService; + _postService = timelinePostService; + _mapper = mapper; + _markdownProcessor = markdownProcessor; + _timelineHubContext = timelineHubContext; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task>> ListAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromQuery] DateTime? modifiedSince, + [FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) + { + return Forbid(); + } + var postPage = await _postService.GetPostsV2Async(timelineId, modifiedSince, page, pageSize); + var items = await _mapper.MapListAsync(postPage.Items, Url, User); + return postPage.WithItems(items); + } + + [HttpGet("{post}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status410Gone)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> GetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute(Name = "post")] long postId) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) + { + return Forbid(); + } + var post = await _postService.GetPostV2Async(timelineId, postId); + var result = await _mapper.MapAsync(post, Url, User); + return result; + } + + [HttpGet("{post}/data")] + [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status410Gone)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> DataIndexGetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post) + { + return await DataGetAsync(owner, timeline, post, 0); + } + + [HttpGet("{post}/data/{data_index}")] + [Produces(MimeTypes.ImagePng, MimeTypes.ImageJpeg, MimeTypes.ImageGif, MimeTypes.ImageWebp, MimeTypes.TextPlain, MimeTypes.TextMarkdown, MimeTypes.TextPlain, MimeTypes.ApplicationJson)] + [ProducesResponseType(typeof(byte[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status304NotModified)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status410Gone)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task DataGetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post, [FromRoute(Name = "data_index")][Range(0, 100)] long dataIndex) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) + { + return Forbid(); + } + + return await DataCacheHelper.GenerateActionResult(this, + () => _postService.GetPostDataDigestV2Async(timelineId, post, dataIndex), + async () => + { + var data = await _postService.GetPostDataV2Async(timelineId, post, dataIndex); + if (data.ContentType == MimeTypes.TextMarkdown) + { + return new ByteData(_markdownProcessor.Process(data.Data, Url, timeline, post), data.ContentType); + } + return data; + } + ); + } + + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> PostAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.IsMemberOfAsync(timelineId, GetAuthUserId())) + { + return Forbid(); + } + + var createRequest = new TimelinePostCreateRequest() + { + Time = body.Time, + Color = body.Color + }; + + for (int i = 0; i < body.DataList.Count; i++) + { + var data = body.DataList[i]; + + if (data is null) + return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is null.")); + + try + { + var d = Convert.FromBase64String(data.Data); + createRequest.DataList.Add(new TimelinePostCreateRequestData(data.ContentType, d)); + } + catch (FormatException) + { + return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is not a valid base64 string.")); + } + } + + try + { + var post = await _postService.CreatePostAsync(timelineId, GetAuthUserId(), createRequest); + + var group = TimelineHub.GenerateTimelinePostChangeListeningGroupName(timeline); + await _timelineHubContext.Clients.Group(group).SendAsync(nameof(ITimelineClient.OnTimelinePostChanged), timeline); + + var result = await _mapper.MapAsync(post, Url, User); + return CreatedAtAction("Get", new { owner = owner, timeline = timeline, post = post.LocalId }, result); + } + catch (TimelinePostCreateDataException e) + { + return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {e.Index} is invalid. {e.Message}")); + } + } + + [HttpPatch("{post}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> Patch([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post, [FromBody] HttpTimelinePostPatchRequest body) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) + { + return Forbid(); + } + + var entity = await _postService.PatchPostAsync(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); + var result = await _mapper.MapAsync(entity, Url, User); + + return Ok(result); + } + + [HttpDelete("{post}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task Delete([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute] long post) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) + { + return Forbid(); + } + + await _postService.DeletePostAsync(timelineId, post); + + return NoContent(); + } + } +} + diff --git a/BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs new file mode 100644 index 00000000..393446f7 --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs @@ -0,0 +1,152 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Timeline.Entities; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Mapper; +using Timeline.Services.Timeline; +using Timeline.Services.User; + +namespace Timeline.Controllers.V2 +{ + [ApiController] + [Route("v2/timelines")] + public class TimelineV2Controller : V2ControllerBase + { + private ITimelineService _timelineService; + private IGenericMapper _mapper; + private IUserService _userService; + + public TimelineV2Controller(ITimelineService timelineService, IGenericMapper mapper, IUserService userService) + { + _timelineService = timelineService; + _mapper = mapper; + _userService = userService; + } + + private Task MapAsync(TimelineEntity entity) + { + return _mapper.MapAsync(entity, Url, User); + } + + [HttpGet("{owner}/{timeline}")] + public async Task> GetAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + var t = await _timelineService.GetTimelineAsync(timelineId); + return await MapAsync(t); + } + + [HttpPatch("{owner}/{timeline}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> PatchAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromBody] HttpTimelinePatchRequest body) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) + { + return Forbid(); + } + await _timelineService.ChangePropertyAsync(timelineId, _mapper.AutoMapperMap(body)); + var t = await _timelineService.GetTimelineAsync(timelineId); + return await MapAsync(t); + } + + [HttpDelete("{owner}/{timeline}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task DeleteAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) + { + return Forbid(); + } + await _timelineService.DeleteTimelineAsync(timelineId); + return NoContent(); + } + + [HttpPut("{owner}/{timeline}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task MemberPutAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute][Username] string member) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) + { + return Forbid(); + } + + long userId; + try + { + userId = await _userService.GetUserIdByUsernameAsync(member); + } + catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) + { + return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); + } + await _timelineService.AddMemberAsync(timelineId, userId); + return NoContent(); + } + + [HttpDelete("{owner}/{timeline}/members/{member}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task MemberDeleteAsync([FromRoute][Username] string owner, [FromRoute][TimelineName] string timeline, [FromRoute][Username] string member) + { + var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline); + if (!UserHasPermission(UserPermission.AllTimelineManagement) && !await _timelineService.HasManagePermissionAsync(timelineId, GetAuthUserId())) + { + return Forbid(); + } + + long userId; + try + { + userId = await _userService.GetUserIdByUsernameAsync(member); + } + catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) + { + return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); + } + await _timelineService.RemoveMemberAsync(timelineId, userId); + return NoContent(); + } + + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> TimelineCreate([FromBody] HttpTimelineCreateRequest body) + { + var authUserId = GetAuthUserId(); + var authUser = await _userService.GetUserAsync(authUserId); + var timeline = await _timelineService.CreateTimelineAsync(authUserId, body.Name); + var result = await MapAsync(timeline); + return CreatedAtAction("Get", new { owner = authUser.Username, timeline = body.Name }, result); + } + } +} + diff --git a/BackEnd/Timeline/Controllers/V2/UserV2Controller.cs b/BackEnd/Timeline/Controllers/V2/UserV2Controller.cs new file mode 100644 index 00000000..e556bf8e --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/UserV2Controller.cs @@ -0,0 +1,181 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Timeline.Auth; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services.Mapper; +using Timeline.Services.User; + +namespace Timeline.Controllers.V2 +{ + /// + /// Operations about users. + /// + [ApiController] + [Route("v2/users")] + public class UserV2Controller : V2ControllerBase + { + private readonly IUserService _userService; + private readonly IUserPermissionService _userPermissionService; + private readonly IUserDeleteService _userDeleteService; + private readonly IGenericMapper _mapper; + + public UserV2Controller(IUserService userService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IGenericMapper mapper) + { + _userService = userService; + _userPermissionService = userPermissionService; + _userDeleteService = userDeleteService; + _mapper = mapper; + } + + /// + /// Get all users. + /// + /// All user list. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> ListAsync([FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) + { + var p = await _userService.GetUsersV2Async(page ?? 1, pageSize ?? 20); + var items = await _mapper.MapListAsync(p.Items, Url, User); + return p.WithItems(items); + } + + /// + /// Create a new user. You have to be administrator. + /// + /// The new user's info. + [HttpPost] + [PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> PostAsync([FromBody] HttpUserPostRequest body) + { + var user = await _userService.CreateUserAsync( + new CreateUserParams(body.Username, body.Password) { Nickname = body.Nickname }); + return CreatedAtAction("Get", new { username = body.Username }, await _mapper.MapAsync(user, Url, User)); + } + + /// + /// Get a user's info. + /// + /// Username of the user. + /// User info. + [HttpGet("{username}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> GetAsync([FromRoute][Username] string username) + { + var id = await _userService.GetUserIdByUsernameAsync(username); + var user = await _userService.GetUserAsync(id); + return await _mapper.MapAsync(user, Url, User); + } + + /// + /// Change a user's property. + /// + /// + /// Username of the user to change. + /// The new user info. + [HttpPatch("{username}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> Patch([FromBody] HttpUserPatchRequest body, [FromRoute][Username] string username) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (UserHasPermission(UserPermission.UserManagement)) + { + var user = await _userService.ModifyUserAsync(userId, _mapper.AutoMapperMap(body)); + return await _mapper.MapAsync(user, Url, User); + } + else + { + if (userId != GetAuthUserId()) + return Forbid(); + + if (body.Username is not null) + return Forbid(); + + if (body.Password is not null) + return Forbid(); + + var user = await _userService.ModifyUserAsync(GetAuthUserId(), _mapper.AutoMapperMap(body)); + return await _mapper.MapAsync(user, Url, User); + } + } + + /// + /// Delete a user and all his related data. You have to be administrator. + /// + /// Username of the user to delete. + /// Info of deletion. + [HttpDelete("{username}")] + [PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> Delete([FromRoute][Username] string username) + { + try + { + await _userDeleteService.DeleteUserAsync(username); + return NoContent(); + } + catch (InvalidOperationOnRootUserException) + { + return UnprocessableEntity(); + } + } + + [HttpPut("{username}/permissions/{permission}"), PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> PutUserPermission([FromRoute][Username] string username, [FromRoute] UserPermission permission) + { + try + { + var id = await _userService.GetUserIdByUsernameAsync(username); + await _userPermissionService.AddPermissionToUserAsync(id, permission); + return NoContent(); + } + catch (InvalidOperationOnRootUserException) + { + return UnprocessableEntity(); + } + } + + [HttpDelete("{username}/permissions/{permission}"), PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> DeleteUserPermission([FromRoute][Username] string username, [FromRoute] UserPermission permission) + { + try + { + var id = await _userService.GetUserIdByUsernameAsync(username); + await _userPermissionService.RemovePermissionFromUserAsync(id, permission); + return NoContent(); + } + catch (InvalidOperationOnRootUserException) + { + return UnprocessableEntity(); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs b/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs new file mode 100644 index 00000000..54b9c7c9 --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Timeline.Auth; +using Timeline.Services.User; + +namespace Timeline.Controllers.V2 +{ + public class V2ControllerBase : ControllerBase + { + #region auth + protected bool UserHasPermission(UserPermission permission) + { + return User.HasPermission(permission); + } + + protected long? GetOptionalAuthUserId() + { + return User.GetOptionalUserId(); + } + + protected long GetAuthUserId() + { + return GetOptionalAuthUserId() ?? throw new InvalidOperationException(Resource.ExceptionNoUserId); + } + #endregion + } +} + diff --git a/BackEnd/Timeline/Services/User/IUserService.cs b/BackEnd/Timeline/Services/User/IUserService.cs index 6ea9a4d2..efb61ccd 100644 --- a/BackEnd/Timeline/Services/User/IUserService.cs +++ b/BackEnd/Timeline/Services/User/IUserService.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Timeline.Entities; - +using Timeline.Models; + namespace Timeline.Services.User { public interface IUserService @@ -46,6 +47,8 @@ namespace Timeline.Services.User /// The user info of users. Task> GetUsersAsync(); + Task> GetUsersV2Async(int page, int pageSize); + /// /// Create a user with given info. /// diff --git a/BackEnd/Timeline/Services/User/UserService.cs b/BackEnd/Timeline/Services/User/UserService.cs index d5ee9a2f..a20076d6 100644 --- a/BackEnd/Timeline/Services/User/UserService.cs +++ b/BackEnd/Timeline/Services/User/UserService.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Token; @@ -266,6 +267,18 @@ namespace Timeline.Services.User _logger.LogInformation(Resource.LogChangePassowrd, entity.Username, id); await _userTokenService.RevokeAllTokenByUserIdAsync(id); - } + } + + public async Task> GetUsersV2Async(int page, int pageSize) + { + if (page <= 0) throw new ArgumentOutOfRangeException(nameof(page)); + if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); + + var items = await _database.Users.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + + var totalCount = await _database.Users.CountAsync(); + + return new Page(page, pageSize, totalCount, items); + } } } diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj index ee14de25..5db7a613 100644 --- a/BackEnd/Timeline/Timeline.csproj +++ b/BackEnd/Timeline/Timeline.csproj @@ -199,4 +199,10 @@ Resource.Designer.cs + + + + + + \ No newline at end of file -- cgit v1.2.3