diff options
Diffstat (limited to 'BackEnd/Timeline/Controllers')
-rw-r--r-- | BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs (renamed from BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs) | 9 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs (renamed from BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs) | 29 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs (renamed from BackEnd/Timeline/Controllers/TimelineV2Controller.cs) | 27 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/V2/UserV2Controller.cs | 185 | ||||
-rw-r--r-- | BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs | 54 |
5 files changed, 266 insertions, 38 deletions
diff --git a/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs index c2130b5a..a23a061b 100644 --- a/BackEnd/Timeline/Controllers/TimelineBookmarkV2Controller.cs +++ b/BackEnd/Timeline/Controllers/V2/TimelineBookmarkV2Controller.cs @@ -10,11 +10,11 @@ using Timeline.Services.Api; using Timeline.Services.Timeline; using Timeline.Services.User; -namespace Timeline.Controllers +namespace Timeline.Controllers.V2 { [ApiController] [Route("v2/users/{username}/bookmarks")] - public class TimelineBookmarkV2Controller : MyControllerBase + public class TimelineBookmarkV2Controller : V2ControllerBase { private readonly IUserService _userService; private readonly ITimelineService _timelineService; @@ -32,7 +32,8 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] [HttpGet] - public async Task<ActionResult<Page<TimelineBookmark>>> ListAsync([FromRoute][Username] string username, [FromQuery] int? page, [FromQuery] int? pageSize) + 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())) @@ -47,7 +48,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] [HttpGet("{index}")] - public async Task<ActionResult<TimelineBookmark>> GetAsync([FromRoute][Username] string username, [FromRoute] int 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())) diff --git a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs index 435ffece..4d486041 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostV2Controller.cs +++ b/BackEnd/Timeline/Controllers/V2/TimelinePostV2Controller.cs @@ -9,31 +9,27 @@ 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 +namespace Timeline.Controllers.V2 { [ApiController] [Route("v2/timelines/{owner}/{timeline}/posts")] - public class TimelinePostV2Controller : MyControllerBase + 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) + public TimelinePostV2Controller(ITimelineService timelineService, ITimelinePostService timelinePostService, MarkdownProcessor markdownProcessor, IHubContext<TimelineHub> timelineHubContext) { _timelineService = timelineService; _postService = timelinePostService; - _mapper = mapper; _markdownProcessor = markdownProcessor; _timelineHubContext = timelineHubContext; } @@ -43,15 +39,16 @@ namespace Timeline.Controllers [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][Range(0, int.MaxValue)] int? page, [FromQuery][Range(1, int.MaxValue)] int? numberPerPage) + 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, numberPerPage); - var items = await _mapper.MapListAsync<HttpTimelinePost>(postPage.Items, Url, User); + var postPage = await _postService.GetPostsV2Async(timelineId, modifiedSince, page, pageSize); + var items = await MapListAsync<HttpTimelinePost>(postPage.Items); return postPage.WithItems(items); } @@ -69,7 +66,7 @@ namespace Timeline.Controllers return Forbid(); } var post = await _postService.GetPostV2Async(timelineId, postId); - var result = await _mapper.MapAsync<HttpTimelinePost>(post, Url, User); + var result = await MapAsync<HttpTimelinePost>(post); return result; } @@ -143,7 +140,7 @@ namespace Timeline.Controllers var data = body.DataList[i]; if (data is null) - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is null.")); + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, $"Data at index {i} is null.")); try { @@ -152,7 +149,7 @@ namespace Timeline.Controllers } catch (FormatException) { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, $"Data at index {i} is not a valid base64 string.")); + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, $"Data at index {i} is not a valid base64 string.")); } } @@ -163,12 +160,12 @@ namespace Timeline.Controllers var group = TimelineHub.GenerateTimelinePostChangeListeningGroupName(timeline); await _timelineHubContext.Clients.Group(group).SendAsync(nameof(ITimelineClient.OnTimelinePostChanged), timeline); - var result = await _mapper.MapAsync<HttpTimelinePost>(post, Url, User); + var result = await MapAsync<HttpTimelinePost>(post); 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}")); + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, $"Data at index {e.Index} is invalid. {e.Message}")); } } @@ -188,7 +185,7 @@ namespace Timeline.Controllers } var entity = await _postService.PatchPostAsync(timelineId, post, new TimelinePostPatchRequest { Time = body.Time, Color = body.Color }); - var result = await _mapper.MapAsync<HttpTimelinePost>(entity, Url, User); + var result = await MapAsync<HttpTimelinePost>(entity); return Ok(result); } diff --git a/BackEnd/Timeline/Controllers/TimelineV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs index 9811cbed..7bc02dc2 100644 --- a/BackEnd/Timeline/Controllers/TimelineV2Controller.cs +++ b/BackEnd/Timeline/Controllers/V2/TimelineV2Controller.cs @@ -2,42 +2,33 @@ 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 +namespace Timeline.Controllers.V2 { [ApiController] [Route("v2/timelines")] - public class TimelineV2Controller : MyControllerBase + public class TimelineV2Controller : V2ControllerBase { private ITimelineService _timelineService; - private IGenericMapper _mapper; private IUserService _userService; - public TimelineV2Controller(ITimelineService timelineService, IGenericMapper mapper, IUserService userService) + public TimelineV2Controller(ITimelineService timelineService, 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); + return await MapAsync<HttpTimeline>(t); } [HttpPatch("{owner}/{timeline}")] @@ -54,9 +45,9 @@ namespace Timeline.Controllers { return Forbid(); } - await _timelineService.ChangePropertyAsync(timelineId, _mapper.AutoMapperMap<TimelineChangePropertyParams>(body)); + await _timelineService.ChangePropertyAsync(timelineId, AutoMapperMap<TimelineChangePropertyParams>(body)); var t = await _timelineService.GetTimelineAsync(timelineId); - return await MapAsync(t); + return await MapAsync<HttpTimeline>(t); } [HttpDelete("{owner}/{timeline}")] @@ -99,7 +90,7 @@ namespace Timeline.Controllers } catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, "Member username does not exist.")); } await _timelineService.AddMemberAsync(timelineId, userId); return NoContent(); @@ -127,7 +118,7 @@ namespace Timeline.Controllers } catch (EntityNotExistException e) when (e.EntityType.Equals(EntityTypes.User)) { - return UnprocessableEntity(new CommonResponse(ErrorCodes.Common.InvalidModel, "Member username does not exist.")); + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, "Member username does not exist.")); } await _timelineService.RemoveMemberAsync(timelineId, userId); return NoContent(); @@ -144,7 +135,7 @@ namespace Timeline.Controllers var authUserId = GetAuthUserId(); var authUser = await _userService.GetUserAsync(authUserId); var timeline = await _timelineService.CreateTimelineAsync(authUserId, body.Name); - var result = await MapAsync(timeline); + var result = await MapAsync<HttpTimeline>(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..40657ad1 --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/UserV2Controller.cs @@ -0,0 +1,185 @@ +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; +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; + + public UserV2Controller(IUserService userService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService) + { + _userService = userService; + _userPermissionService = userPermissionService; + _userDeleteService = userDeleteService; + } + + /// <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 MapListAsync<HttpUser>(p.Items); + 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 MapAsync<HttpUser>(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 MapAsync<HttpUser>(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>> PatchAsync([FromBody] HttpUserPatchRequest body, [FromRoute][Username] string username) + { + var userId = await _userService.GetUserIdByUsernameAsync(username); + if (UserHasPermission(UserPermission.UserManagement)) + { + var user = await _userService.ModifyUserAsync(userId, AutoMapperMap<ModifyUserParams>(body)); + return await MapAsync<HttpUser>(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(), AutoMapperMap<ModifyUserParams>(body)); + return await MapAsync<HttpUser>(user); + } + } + + private const string RootUserInvalidOperationMessage = "Can't do this operation on root 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> DeleteAsync([FromRoute][Username] string username) + { + try + { + await _userDeleteService.DeleteUserAsync(username); + return NoContent(); + } + catch (EntityNotExistException) + { + return NoContent(); + } + catch (InvalidOperationOnRootUserException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidOperation, RootUserInvalidOperationMessage)); + } + } + + [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> PutUserPermissionAsync([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(new ErrorResponse(ErrorResponse.InvalidOperation, RootUserInvalidOperationMessage)); + } + } + + [HttpDelete("{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> DeleteUserPermissionAsync([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(new ErrorResponse(ErrorResponse.InvalidOperation, RootUserInvalidOperationMessage)); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs b/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs new file mode 100644 index 00000000..d6fa0c84 --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Timeline.Auth; +using Timeline.Services.Mapper; +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 + + #region mapper + protected IGenericMapper GetMapper() + { + return HttpContext.RequestServices.GetRequiredService<IGenericMapper>(); + } + + protected async Task<T> MapAsync<T>(object o) + { + return await GetMapper().MapAsync<T>(o, Url, User); + } + + protected async Task<List<T>> MapListAsync<T>(IEnumerable<object> o) + { + return await GetMapper().MapListAsync<T>(o, Url, User); + } + + protected T AutoMapperMap<T>(object o) + { + return GetMapper().AutoMapperMap<T>(o); + } + #endregion + } +} + |