diff options
Diffstat (limited to 'BackEnd/Timeline/Controllers/V2')
5 files changed, 756 insertions, 0 deletions
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<ActionResult<Page<TimelineBookmark>>> 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<ActionResult<TimelineBookmark>> 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<ActionResult<TimelineBookmark>> 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<ActionResult> 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<ActionResult<TimelineBookmark>> 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<ActionResult<HttpTimelineBookmarkVisibility>> 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<ActionResult> 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<TimelineHub> _timelineHubContext; + + public TimelinePostV2Controller(ITimelineService timelineService, ITimelinePostService timelinePostService, IGenericMapper mapper, MarkdownProcessor markdownProcessor, IHubContext<TimelineHub> 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<ActionResult<Page<HttpTimelinePost>>> 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<HttpTimelinePost>(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<ActionResult<HttpTimelinePost>> 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<HttpTimelinePost>(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<ActionResult<ByteData>> 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<ActionResult> 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<ActionResult<HttpTimelinePost>> 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<HttpTimelinePost>(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<ActionResult<HttpTimelinePost>> 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<HttpTimelinePost>(entity, Url, User); + + return Ok(result); + } + + [HttpDelete("{post}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult> 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<HttpTimeline> MapAsync(TimelineEntity entity) + { + return _mapper.MapAsync<HttpTimeline>(entity, Url, User); + } + + [HttpGet("{owner}/{timeline}")] + public async Task<ActionResult<HttpTimeline>> 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<ActionResult<HttpTimeline>> 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<TimelineChangePropertyParams>(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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult<HttpTimeline>> 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 +{ + /// <summary> + /// Operations about users. + /// </summary> + [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; + } + + /// <summary> + /// Get all users. + /// </summary> + /// <returns>All user list.</returns> + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult<Page<HttpUser>>> ListAsync([FromQuery][PositiveInteger] int? page, [FromQuery][PositiveInteger] int? pageSize) + { + var p = await _userService.GetUsersV2Async(page ?? 1, pageSize ?? 20); + var items = await _mapper.MapListAsync<HttpUser>(p.Items, Url, User); + return p.WithItems(items); + } + + /// <summary> + /// Create a new user. You have to be administrator. + /// </summary> + /// <returns>The new user's info.</returns> + [HttpPost] + [PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpUser>> 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<HttpUser>(user, Url, User)); + } + + /// <summary> + /// Get a user's info. + /// </summary> + /// <param name="username">Username of the user.</param> + /// <returns>User info.</returns> + [HttpGet("{username}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpUser>> GetAsync([FromRoute][Username] string username) + { + var id = await _userService.GetUserIdByUsernameAsync(username); + var user = await _userService.GetUserAsync(id); + return await _mapper.MapAsync<HttpUser>(user, Url, User); + } + + /// <summary> + /// Change a user's property. + /// </summary> + /// <param name="body"></param> + /// <param name="username">Username of the user to change.</param> + /// <returns>The new user info.</returns> + [HttpPatch("{username}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpUser>> 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<ModifyUserParams>(body)); + return await _mapper.MapAsync<HttpUser>(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<ModifyUserParams>(body)); + return await _mapper.MapAsync<HttpUser>(user, Url, User); + } + } + + /// <summary> + /// Delete a user and all his related data. You have to be administrator. + /// </summary> + /// <param name="username">Username of the user to delete.</param> + /// <returns>Info of deletion.</returns> + [HttpDelete("{username}")] + [PermissionAuthorize(UserPermission.UserManagement)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<CommonDeleteResponse>> 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<ActionResult<CommonResponse>> 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<ActionResult<CommonResponse>> 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 + } +} + |