aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BackEnd/Timeline.Tests/Timeline.Tests.csproj6
-rw-r--r--BackEnd/Timeline.Tests/packages.lock.json116
-rw-r--r--BackEnd/Timeline/Configs/TokenOptions.cs5
-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.cs185
-rw-r--r--BackEnd/Timeline/Controllers/V2/V2ControllerBase.cs54
-rw-r--r--BackEnd/Timeline/Filters/CatchEntityAlreadyExistExceptionFilter.cs2
-rw-r--r--BackEnd/Timeline/Filters/CatchEntityDeletedExceptionFilter.cs2
-rw-r--r--BackEnd/Timeline/Filters/CatchEntityNotExistExceptionFilter.cs2
-rw-r--r--BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs2
-rw-r--r--BackEnd/Timeline/Models/Http/ErrorResponse.cs19
-rw-r--r--BackEnd/Timeline/Models/Validation/PositiveIntegerAttribute.cs12
-rw-r--r--BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs15
-rw-r--r--BackEnd/Timeline/Services/User/IUserService.cs5
-rw-r--r--BackEnd/Timeline/Services/User/UserService.cs15
-rw-r--r--BackEnd/Timeline/Timeline.csproj16
-rw-r--r--BackEnd/Timeline/packages.lock.json66
-rw-r--r--FrontEnd/src/http/timeline.ts16
20 files changed, 452 insertions, 151 deletions
diff --git a/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
index 4d4d3da2..84fc9055 100644
--- a/BackEnd/Timeline.Tests/Timeline.Tests.csproj
+++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
@@ -14,9 +14,9 @@
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.2.0" />
- <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
- <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
- <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.3" />
+ <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.8" />
+ <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.4" />
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit" Version="2.4.1" />
diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json
index aef482ad..f1679705 100644
--- a/BackEnd/Timeline.Tests/packages.lock.json
+++ b/BackEnd/Timeline.Tests/packages.lock.json
@@ -30,9 +30,9 @@
},
"Microsoft.AspNet.WebApi.Client": {
"type": "Direct",
- "requested": "[5.2.7, )",
- "resolved": "5.2.7",
- "contentHash": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==",
+ "requested": "[5.2.8, )",
+ "resolved": "5.2.8",
+ "contentHash": "dkGLm30CxLieMlUO+oQpJw77rDs0IIx/w3lIHsp+8X94HXCsUfLYuFmlZPsQDItC0O2l1ZlWeKLkZX7ZiNRekw==",
"dependencies": {
"Newtonsoft.Json": "10.0.1",
"Newtonsoft.Json.Bson": "1.0.1"
@@ -40,19 +40,19 @@
},
"Microsoft.AspNetCore.SignalR.Client": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "SDzFc3+eOHcdotjVKXeM/XlOvTc0s+uUe19g5uYEtJ2SQq/7m6muEUhXvZ/U5JlHr9oKuNdqJeJUt7ZQoQichA==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "LYmOnYvNPmtjg3pDaG9TnlE92GwU/yKP+LJqK9FLiPXCmcrs6jrgZpiPry7lohzAC35VRFn5V7WXHZ9LhF6PGA==",
"dependencies": {
- "Microsoft.AspNetCore.Http.Connections.Client": "6.0.3",
- "Microsoft.AspNetCore.SignalR.Client.Core": "6.0.3"
+ "Microsoft.AspNetCore.Http.Connections.Client": "6.0.4",
+ "Microsoft.AspNetCore.SignalR.Client.Core": "6.0.4"
}
},
"Microsoft.AspNetCore.TestHost": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "oeK3KFtoLslFbKyQEiQ8HgKWAmTXUlp8vtYByUmC5ysWVrNEpCrQOKACGV/uYSeC9aFvEIvVyFTcommnxrsyiA==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "glganOCWeDVyPjy3D2K+Tpp5Xkwglv1I5aAT+nHh6Cq1fKlilibMf+yruBXogOZwasJIOgXzG4CLqVVXNoSpew==",
"dependencies": {
"System.IO.Pipelines": "6.0.2"
}
@@ -135,38 +135,38 @@
},
"Microsoft.AspNetCore.Connections.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "sKw40x8OW/WzJPq9CAlQ+7QQFHuoBVeGzmyPtUyBDeyTcBe43rhFRNJuRJYbOQL72M3BKR6wle7R/AAWKNwmxQ==",
+ "resolved": "6.0.4",
+ "contentHash": "Vaps21IhEI6rP1vR1/3aPnIJZmS/xXIx0vgJzEnvzjBGz47x3ercO5HTSat6cyq7sR0n7N1fNoYhP9gzyrZ8tQ==",
"dependencies": {
- "Microsoft.Extensions.Features": "6.0.3",
+ "Microsoft.Extensions.Features": "6.0.4",
"System.IO.Pipelines": "6.0.2"
}
},
"Microsoft.AspNetCore.Http.Connections.Client": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "lSL0JaFtZXOUYuttpsP8hwNTWxGH8xt7qSAFElS4mNUq/PAXw/BwLKD0md+zHm6w2ExI+NWrGDQeQzU3T/C+qQ==",
+ "resolved": "6.0.4",
+ "contentHash": "HIwfReamEUBTOXZ7BB/3KsFZa6OT7Bs8gNpufw2QdyVpJZej7OG4OCVQ3bik2l+RE+EqA3hftp5+FBToDsE6Ag==",
"dependencies": {
- "Microsoft.AspNetCore.Http.Connections.Common": "6.0.3",
+ "Microsoft.AspNetCore.Http.Connections.Common": "6.0.4",
"Microsoft.Extensions.Logging.Abstractions": "6.0.1",
"Microsoft.Extensions.Options": "6.0.0"
}
},
"Microsoft.AspNetCore.Http.Connections.Common": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "ig3U4mq+88/nEfBGQvx2zEbqJGSfdb7BA28kkiyPw8n2bmcbg8dHVXw9CzCe3iNtth4FaNuyfb7Apm06pv4Tkg==",
+ "resolved": "6.0.4",
+ "contentHash": "/I9iwOvD/jcSmWNYoAF0s1RHolVF42gnNF26P0DaF58RvGjcFQ7CfJzWykAnJRRCn0zyDhetx6TL3/dbao1DgA==",
"dependencies": {
- "Microsoft.AspNetCore.Connections.Abstractions": "6.0.3"
+ "Microsoft.AspNetCore.Connections.Abstractions": "6.0.4"
}
},
"Microsoft.AspNetCore.SignalR.Client.Core": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "k+M/fBSI/P1g7UOSjyNBjMgnpq2WKMCYtgFvwX5LeVmb6RPy6HjJorFQK8R23wwZbyEBv/IufdDWdPJqBT9U5w==",
+ "resolved": "6.0.4",
+ "contentHash": "Nwxqnh1uR2Lkba7iEs8ZlP/Tevee9+ZRSpmeaS9IoWnOGITwoBhduO0IfZm8/JTC4jp44du7Tfa7xuRSaqCTmA==",
"dependencies": {
- "Microsoft.AspNetCore.SignalR.Common": "6.0.3",
- "Microsoft.AspNetCore.SignalR.Protocols.Json": "6.0.3",
+ "Microsoft.AspNetCore.SignalR.Common": "6.0.4",
+ "Microsoft.AspNetCore.SignalR.Protocols.Json": "6.0.4",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
"System.Threading.Channels": "6.0.0"
@@ -174,25 +174,25 @@
},
"Microsoft.AspNetCore.SignalR.Common": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "0UTSbl8+dmLh/vhujCkz1SeX2k0y5mBq1CfkjCSNVJV3G/Ve6ynbTt10zytiLYECWQ/iTOGs7+bWnfAIMn2OFQ==",
+ "resolved": "6.0.4",
+ "contentHash": "E08fDZnbZ3VUfS7l/sws6C8oGpkueB0IFmbZBJueMj1guS0JYfhQpMqAhuZiHG9lzY7HMqZigqpe49eF7DJaqA==",
"dependencies": {
- "Microsoft.AspNetCore.Connections.Abstractions": "6.0.3",
+ "Microsoft.AspNetCore.Connections.Abstractions": "6.0.4",
"Microsoft.Extensions.Options": "6.0.0"
}
},
"Microsoft.AspNetCore.SignalR.Protocols.Json": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "JFZv4ZxEuselWL8yqW2b7QZj3MBj5fhrsHtUHphlSWS0NEwfIX1ecrBpubitT4i7Fqgzh7VGqXblmCpGZMhMNw==",
+ "resolved": "6.0.4",
+ "contentHash": "YBZ2x8nNBy+AAETpdiQebjK0z7QIyraUMfHxd12BCG8MrLbqJTH8XYK8AiWyodBC939T+8p+mO2nw+oDZDCrcw==",
"dependencies": {
- "Microsoft.AspNetCore.SignalR.Common": "6.0.3"
+ "Microsoft.AspNetCore.SignalR.Common": "6.0.4"
}
},
"Microsoft.AspNetCore.SpaServices.Extensions": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "CJSN7LTL5w9ptrBfRF3F0IJTNRjjNBk+Iy1AZoWrvim3iLW/+EHF8SAjMP4fXvJI4iIXz5jFAqrHSB1P49qXNg==",
+ "resolved": "6.0.4",
+ "contentHash": "5SmJJqVlHfybZ3nXE7+VthSSYr6s8sEJDiTTD63+cVWVnWPYOXpXKrt3cVFDvhXplFdmQkMKCMYNzJ7pRVkJsA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Physical": "6.0.0"
}
@@ -209,19 +209,19 @@
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "3EIhLzG+WeJ2AtmGph8aSA5pH9weDOZ9Fox6oaDSiPvj6TIehzYkdW0ySMAfckTbv8V1vCMeEmtqhwEmyK9fYA==",
+ "resolved": "6.0.4",
+ "contentHash": "3TZX7R2aX1TX5m4A5Kj+SY633NJDeHDP6JiDRCwUnJGKC3IrHgnO8p+oT2hRZpN168qx4Ixe4T9C+xZdZc26gw==",
"dependencies": {
"SQLitePCLRaw.core": "2.0.6"
}
},
"Microsoft.EntityFrameworkCore": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "f7ZlNOI1wPg/r0zRgvmcLAM3kwuCVGe5cZc8rYoKPDGZs5dgRFe5a43H9FnCXsyFAF6FILB08Nr9RkuNMwd3TQ==",
+ "resolved": "6.0.4",
+ "contentHash": "gTh3SJsF5WNjEmG32kYc3U4tjeTIv55QOrwHAJcF/xtrIVMteDHMArGC35N0dw86WFY0v8yFkKYKOIOln4jkfQ==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "6.0.3",
- "Microsoft.EntityFrameworkCore.Analyzers": "6.0.3",
+ "Microsoft.EntityFrameworkCore.Abstractions": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Analyzers": "6.0.4",
"Microsoft.Extensions.Caching.Memory": "6.0.1",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
@@ -231,39 +231,39 @@
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "7c6wJtTFj7FDe20IVPDKeCzxWIMu+XDpYi7aYuUo6knu4vDeB3N3F0pjAZgXJ96GwTZYBrjOEwnbpYmfapEz/w=="
+ "resolved": "6.0.4",
+ "contentHash": "jycTQF0FUJp10cGWBmtsyFhQNeISU9CltDRKCaNiX4QRSEFzgRgaFN4vAFK0T+G5etmXugyddijE4NWCGtgznQ=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "Ld9HyVABYor3Tq43/sG2JERL94UnDHJPg5PwpkCW4CWsf/CbxHetdecv+bCXA/Hg4q+44sov0GVl4Ywjk4PnyA=="
+ "resolved": "6.0.4",
+ "contentHash": "t12WodVyGGP2CuLo7R1qwcawHY5zlg+GiQzvkceZpsjcFJVyTFFBFDPg1isBtzurLzWsl+G3z5fVXeic90mPxg=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "Dga4OH8QxmPT9APr7ZL5R0TvNADOcjDNNvJa8pU3QbPJ6OvRPTlzqAehfEBow0/NXBTKbIO48b4aIJI4VCO7fg==",
+ "resolved": "6.0.4",
+ "contentHash": "E867NbEXYRTElBF5ff+1AN5Awa1jkORy/Rrm0ueibaTAV5uw89LsLoH6yTe+b9urZTWMHtLfGd1RDdNjk8+KzA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "6.0.3",
+ "Microsoft.EntityFrameworkCore": "6.0.4",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "Zz0t+iBdC64fQeBOHr+5IpR/SNhfmxOjM0aI8VA5Xk8wBS/o98YWVXp8kT1iGWS3+K2jDGuusl192LZthLUStQ==",
+ "resolved": "6.0.4",
+ "contentHash": "nn3UB4PxqnECcEWJPcIKrcuGnevf/lPd/LqFgLVYgqYe8teVqpI/yCKSDOPkEkrAbjWAB0Sgx+q59XpT5YphKQ==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.3",
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.4",
"SQLitePCLRaw.bundle_e_sqlite3": "2.0.6"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "9h4/Icp2jsrhxo6PPeEV0B5UwT24ZwvV1cbdGW7u2YCwl0e8ToBw+IaADtER8l6vwH9y210+oMHmTGOP/kHfyQ==",
+ "resolved": "6.0.4",
+ "contentHash": "yO6XZA8FQkBLiTtClesWClL3Z1QEqT9vRoDDf/IOPkmsjzjedg2GzkNCauRGk/XnsZZ1qcz55TuJBO+1jNDIaA==",
"dependencies": {
- "Microsoft.Data.Sqlite.Core": "6.0.3",
- "Microsoft.EntityFrameworkCore.Relational": "6.0.3",
+ "Microsoft.Data.Sqlite.Core": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Relational": "6.0.4",
"Microsoft.Extensions.DependencyModel": "6.0.0"
}
},
@@ -328,8 +328,8 @@
},
"Microsoft.Extensions.Features": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "pfVzM2jpJgXwS7dHgUfEi4yUXGpeEHCSSgGa1Zotug8Kzdf2sPIaOfJHqnLpWIFSdx3Fa7zuBZ8X3eBDvCjuDA=="
+ "resolved": "6.0.4",
+ "contentHash": "XqUVkCosnG7wQm6/Pk8soCILi8/ENTsALWV8VaAb2pcWqGmv6YPZP4IwCzWRjDkcpBOZ7Wgs2xJeWssn2YP+0Q=="
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
@@ -1756,10 +1756,10 @@
"AutoMapper": "11.0.1",
"AutoMapper.Extensions.Microsoft.DependencyInjection": "11.0.0",
"Markdig": "0.28.1",
- "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.3",
- "Microsoft.EntityFrameworkCore": "6.0.3",
- "Microsoft.EntityFrameworkCore.Analyzers": "6.0.3",
- "Microsoft.EntityFrameworkCore.Sqlite": "6.0.3",
+ "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.4",
+ "Microsoft.EntityFrameworkCore": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Analyzers": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Sqlite": "6.0.4",
"NSwag.AspNetCore": "13.15.10",
"SixLabors.ImageSharp": "2.1.0",
"System.IdentityModel.Tokens.Jwt": "6.17.0"
diff --git a/BackEnd/Timeline/Configs/TokenOptions.cs b/BackEnd/Timeline/Configs/TokenOptions.cs
index d8e968c7..d643c3d2 100644
--- a/BackEnd/Timeline/Configs/TokenOptions.cs
+++ b/BackEnd/Timeline/Configs/TokenOptions.cs
@@ -3,9 +3,8 @@
public class TokenOptions
{
/// <summary>
- /// The length of the generated secure random token counted in byte.
- /// Note the byte will be converted to hex form when used.
- /// Default is 32 byte long.
+ /// The length of the token.
+ /// Default is 16.
/// </summary>
public long? TokenLength { get; set; }
}
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
+ }
+}
+
diff --git a/BackEnd/Timeline/Filters/CatchEntityAlreadyExistExceptionFilter.cs b/BackEnd/Timeline/Filters/CatchEntityAlreadyExistExceptionFilter.cs
index a1d1c0e6..469f2de5 100644
--- a/BackEnd/Timeline/Filters/CatchEntityAlreadyExistExceptionFilter.cs
+++ b/BackEnd/Timeline/Filters/CatchEntityAlreadyExistExceptionFilter.cs
@@ -23,7 +23,7 @@ namespace Timeline.Filters
{
if (context.HttpContext.Request.Path.StartsWithSegments("/api/v2"))
{
- context.Result = new UnprocessableEntityObjectResult(new CommonResponse(ErrorCodes.Conflict.Default, "An entity with given key already exists."));
+ context.Result = new UnprocessableEntityObjectResult(new ErrorResponse(ErrorResponse.EntityExist, "An entity with given key already exists."));
}
else
{
diff --git a/BackEnd/Timeline/Filters/CatchEntityDeletedExceptionFilter.cs b/BackEnd/Timeline/Filters/CatchEntityDeletedExceptionFilter.cs
index 41f4894d..5a6c75ec 100644
--- a/BackEnd/Timeline/Filters/CatchEntityDeletedExceptionFilter.cs
+++ b/BackEnd/Timeline/Filters/CatchEntityDeletedExceptionFilter.cs
@@ -9,7 +9,7 @@ namespace Timeline.Filters
{
public void OnException(ExceptionContext context)
{
- if (context.Exception is EntityDeletedException e)
+ if (context.Exception is EntityDeletedException)
{
context.Result = new StatusCodeResult(StatusCodes.Status410Gone);
}
diff --git a/BackEnd/Timeline/Filters/CatchEntityNotExistExceptionFilter.cs b/BackEnd/Timeline/Filters/CatchEntityNotExistExceptionFilter.cs
index 876f792a..01a3d52d 100644
--- a/BackEnd/Timeline/Filters/CatchEntityNotExistExceptionFilter.cs
+++ b/BackEnd/Timeline/Filters/CatchEntityNotExistExceptionFilter.cs
@@ -25,7 +25,7 @@ namespace Timeline.Filters
{
if (context.HttpContext.Request.Path.StartsWithSegments("/api/v2"))
{
- context.Result = new NotFoundObjectResult(new CommonResponse(ErrorCodes.NotExist.Default, "The entity does not exist."));
+ context.Result = new NotFoundResult();
}
else
{
diff --git a/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs
index 906ac566..d7dca9d1 100644
--- a/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs
+++ b/BackEnd/Timeline/Helpers/InvalidModelResponseFactory.cs
@@ -10,7 +10,7 @@ namespace Timeline.Helpers
{
if (context.HttpContext.Request.Path.StartsWithSegments("/api/v2"))
{
- return new UnprocessableEntityObjectResult(new CommonResponse(ErrorCodes.Common.InvalidModel, "Request is of bad format."));
+ return new UnprocessableEntityObjectResult(new ErrorResponse(ErrorResponse.InvalidRequest, "Request is of bad format."));
}
var modelState = context.ModelState;
diff --git a/BackEnd/Timeline/Models/Http/ErrorResponse.cs b/BackEnd/Timeline/Models/Http/ErrorResponse.cs
new file mode 100644
index 00000000..119e3977
--- /dev/null
+++ b/BackEnd/Timeline/Models/Http/ErrorResponse.cs
@@ -0,0 +1,19 @@
+namespace Timeline.Models.Http
+{
+ public class ErrorResponse
+ {
+ public const string InvalidRequest = "INVALID_REQUEST";
+ public const string EntityExist = "ENTITY_EXIST";
+ public const string InvalidOperation = "INVALID_OPERATION";
+
+ public ErrorResponse(string error, string message)
+ {
+ Error = error;
+ Message = message;
+ }
+
+ public string Error { get; set; }
+
+ public string Message { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Models/Validation/PositiveIntegerAttribute.cs b/BackEnd/Timeline/Models/Validation/PositiveIntegerAttribute.cs
new file mode 100644
index 00000000..78e2f0b4
--- /dev/null
+++ b/BackEnd/Timeline/Models/Validation/PositiveIntegerAttribute.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Timeline.Models.Validation
+{
+ public class PositiveIntegerAttribute : RangeAttribute
+ {
+ public PositiveIntegerAttribute() : base(1, int.MaxValue)
+ {
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
index 2ab263de..4d79295a 100644
--- a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
+++ b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -33,13 +34,23 @@ namespace Timeline.Services.Token
_secureRandom.Dispose();
}
+ private static readonly char[] AlphaDigitString = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
+
private string GenerateSecureRandomTokenString()
{
var option = _optionMonitor.CurrentValue;
- var tokenLength = option.TokenLength ?? 32;
+ var tokenLength = option.TokenLength ?? 16;
var buffer = new byte[tokenLength];
_secureRandom.GetBytes(buffer);
- return Convert.ToHexString(buffer);
+
+ StringBuilder stringBuilder = new();
+
+ foreach (byte b in buffer)
+ {
+ stringBuilder.Append(AlphaDigitString[b % AlphaDigitString.Length]);
+ }
+
+ return stringBuilder.ToString();
}
/// <inheritdoc/>
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
/// <returns>The user info of users.</returns>
Task<List<UserEntity>> GetUsersAsync();
+ Task<Page<UserEntity>> GetUsersV2Async(int page, int pageSize);
+
/// <summary>
/// Create a user with given info.
/// </summary>
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<Page<UserEntity>> 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<UserEntity>(page, pageSize, totalCount, items);
+ }
}
}
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj
index ee14de25..28234a4d 100644
--- a/BackEnd/Timeline/Timeline.csproj
+++ b/BackEnd/Timeline/Timeline.csproj
@@ -37,11 +37,11 @@
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Markdig" Version="0.28.1" />
- <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="6.0.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
+ <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="6.0.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -199,4 +199,10 @@
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
+ <ItemGroup>
+ <None Remove="Controllers\V2\" />
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="Controllers\V2\" />
+ </ItemGroup>
</Project> \ No newline at end of file
diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json
index 3212d657..cd8384d2 100644
--- a/BackEnd/Timeline/packages.lock.json
+++ b/BackEnd/Timeline/packages.lock.json
@@ -29,21 +29,21 @@
},
"Microsoft.AspNetCore.SpaServices.Extensions": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "CJSN7LTL5w9ptrBfRF3F0IJTNRjjNBk+Iy1AZoWrvim3iLW/+EHF8SAjMP4fXvJI4iIXz5jFAqrHSB1P49qXNg==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "5SmJJqVlHfybZ3nXE7+VthSSYr6s8sEJDiTTD63+cVWVnWPYOXpXKrt3cVFDvhXplFdmQkMKCMYNzJ7pRVkJsA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Physical": "6.0.0"
}
},
"Microsoft.EntityFrameworkCore": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "f7ZlNOI1wPg/r0zRgvmcLAM3kwuCVGe5cZc8rYoKPDGZs5dgRFe5a43H9FnCXsyFAF6FILB08Nr9RkuNMwd3TQ==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "gTh3SJsF5WNjEmG32kYc3U4tjeTIv55QOrwHAJcF/xtrIVMteDHMArGC35N0dw86WFY0v8yFkKYKOIOln4jkfQ==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Abstractions": "6.0.3",
- "Microsoft.EntityFrameworkCore.Analyzers": "6.0.3",
+ "Microsoft.EntityFrameworkCore.Abstractions": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Analyzers": "6.0.4",
"Microsoft.Extensions.Caching.Memory": "6.0.1",
"Microsoft.Extensions.DependencyInjection": "6.0.0",
"Microsoft.Extensions.Logging": "6.0.0",
@@ -53,27 +53,27 @@
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "Ld9HyVABYor3Tq43/sG2JERL94UnDHJPg5PwpkCW4CWsf/CbxHetdecv+bCXA/Hg4q+44sov0GVl4Ywjk4PnyA=="
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "t12WodVyGGP2CuLo7R1qwcawHY5zlg+GiQzvkceZpsjcFJVyTFFBFDPg1isBtzurLzWsl+G3z5fVXeic90mPxg=="
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "Zz0t+iBdC64fQeBOHr+5IpR/SNhfmxOjM0aI8VA5Xk8wBS/o98YWVXp8kT1iGWS3+K2jDGuusl192LZthLUStQ==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "nn3UB4PxqnECcEWJPcIKrcuGnevf/lPd/LqFgLVYgqYe8teVqpI/yCKSDOPkEkrAbjWAB0Sgx+q59XpT5YphKQ==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.3",
+ "Microsoft.EntityFrameworkCore.Sqlite.Core": "6.0.4",
"SQLitePCLRaw.bundle_e_sqlite3": "2.0.6"
}
},
"Microsoft.EntityFrameworkCore.Tools": {
"type": "Direct",
- "requested": "[6.0.3, )",
- "resolved": "6.0.3",
- "contentHash": "s5s19Woo8kFqI9GQrqM6ZT4qS1xLlWuccqfg3ELf8shp93f8btuK6T3iFV//wP1pkRjFB4qbPrQwMO88RKfHXw==",
+ "requested": "[6.0.4, )",
+ "resolved": "6.0.4",
+ "contentHash": "6uAir6kFgkZoM2xsmNxs3WhK03xN37u4fW44X8cAXzllkG4txMajbkYhT0jSh4D+YrnfdNCq3K8ol3MyCCGEug==",
"dependencies": {
- "Microsoft.EntityFrameworkCore.Design": "6.0.3"
+ "Microsoft.EntityFrameworkCore.Design": "6.0.4"
}
},
"NSwag.AspNetCore": {
@@ -123,42 +123,42 @@
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "3EIhLzG+WeJ2AtmGph8aSA5pH9weDOZ9Fox6oaDSiPvj6TIehzYkdW0ySMAfckTbv8V1vCMeEmtqhwEmyK9fYA==",
+ "resolved": "6.0.4",
+ "contentHash": "3TZX7R2aX1TX5m4A5Kj+SY633NJDeHDP6JiDRCwUnJGKC3IrHgnO8p+oT2hRZpN168qx4Ixe4T9C+xZdZc26gw==",
"dependencies": {
"SQLitePCLRaw.core": "2.0.6"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "7c6wJtTFj7FDe20IVPDKeCzxWIMu+XDpYi7aYuUo6knu4vDeB3N3F0pjAZgXJ96GwTZYBrjOEwnbpYmfapEz/w=="
+ "resolved": "6.0.4",
+ "contentHash": "jycTQF0FUJp10cGWBmtsyFhQNeISU9CltDRKCaNiX4QRSEFzgRgaFN4vAFK0T+G5etmXugyddijE4NWCGtgznQ=="
},
"Microsoft.EntityFrameworkCore.Design": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "Gc3XhQ4vLK4rlGpZi8zaa4bl0t894QxqMXHSbDSsx8d7ghRaLbGIH1tQawpo2hriU/q918eepbCyoISD89Xx1w==",
+ "resolved": "6.0.4",
+ "contentHash": "NOAtatJPq/xDa+vqfxL5+OwCKv83N/JX/IPgY6VQZ95KUtV9uuC+mVPsremG3a9UM3tPfK4CXMh1rwiaZHYCPQ==",
"dependencies": {
"Humanizer.Core": "2.8.26",
- "Microsoft.EntityFrameworkCore.Relational": "6.0.3"
+ "Microsoft.EntityFrameworkCore.Relational": "6.0.4"
}
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "Dga4OH8QxmPT9APr7ZL5R0TvNADOcjDNNvJa8pU3QbPJ6OvRPTlzqAehfEBow0/NXBTKbIO48b4aIJI4VCO7fg==",
+ "resolved": "6.0.4",
+ "contentHash": "E867NbEXYRTElBF5ff+1AN5Awa1jkORy/Rrm0ueibaTAV5uw89LsLoH6yTe+b9urZTWMHtLfGd1RDdNjk8+KzA==",
"dependencies": {
- "Microsoft.EntityFrameworkCore": "6.0.3",
+ "Microsoft.EntityFrameworkCore": "6.0.4",
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "9h4/Icp2jsrhxo6PPeEV0B5UwT24ZwvV1cbdGW7u2YCwl0e8ToBw+IaADtER8l6vwH9y210+oMHmTGOP/kHfyQ==",
+ "resolved": "6.0.4",
+ "contentHash": "yO6XZA8FQkBLiTtClesWClL3Z1QEqT9vRoDDf/IOPkmsjzjedg2GzkNCauRGk/XnsZZ1qcz55TuJBO+1jNDIaA==",
"dependencies": {
- "Microsoft.Data.Sqlite.Core": "6.0.3",
- "Microsoft.EntityFrameworkCore.Relational": "6.0.3",
+ "Microsoft.Data.Sqlite.Core": "6.0.4",
+ "Microsoft.EntityFrameworkCore.Relational": "6.0.4",
"Microsoft.Extensions.DependencyModel": "6.0.0"
}
},
diff --git a/FrontEnd/src/http/timeline.ts b/FrontEnd/src/http/timeline.ts
index 0e1ceb81..d9e679ea 100644
--- a/FrontEnd/src/http/timeline.ts
+++ b/FrontEnd/src/http/timeline.ts
@@ -133,7 +133,9 @@ export interface IHttpTimelineClient {
): Promise<void>;
listPost(
ownerUsername: string,
- timelineName: string
+ timelineName: string,
+ page?: number,
+ pageSize?: number
): Promise<Page<HttpTimelineGenericPostInfo>>;
generatePostDataUrl(
ownerUsername: string,
@@ -235,11 +237,19 @@ export class HttpTimelineClient implements IHttpTimelineClient {
listPost(
ownerUsername: string,
- timelineName: string
+ timelineName: string,
+ page?: number,
+ pageSize?: number
): Promise<Page<HttpTimelineGenericPostInfo>> {
return axios
.get<Page<HttpTimelineGenericPostInfo>>(
- `${apiBaseUrl}/v2/timelines/${ownerUsername}/${timelineName}/posts`
+ applyQueryParameters(
+ `${apiBaseUrl}/v2/timelines/${ownerUsername}/${timelineName}/posts`,
+ {
+ page,
+ pageSize,
+ }
+ )
)
.then(extractResponseData);
}