aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline/Controllers/V2
diff options
context:
space:
mode:
Diffstat (limited to 'BackEnd/Timeline/Controllers/V2')
-rw-r--r--BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs177
-rw-r--r--BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs218
-rw-r--r--BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs152
-rw-r--r--BackEnd/Timeline/Controllers/V2/UserV2Controller.cs181
-rw-r--r--BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs28
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
+ }
+}
+