diff options
49 files changed, 2636 insertions, 1218 deletions
diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index 90c4ed99..a8519216 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -63,6 +63,11 @@ public const int PostNotExist = 1_104_05_01;
public const int PostNoData = 1_104_05_02;
}
+
+ public static class HighlightTimelineController
+ {
+ public const int NonHighlight = 1_105_01_01;
+ }
}
}
diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs index 0f873033..da9481f1 100644 --- a/BackEnd/Timeline.Tests/GlobalSuppressions.cs +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -3,14 +3,17 @@ // Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")]
+[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")]
+[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")]
+[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")]
+[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")]
+[assembly: SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")]
+[assembly: SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")]
+[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")]
+[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")]
+[assembly: SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")]
+[assembly: SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")]
+[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "We use another contract like IAsyncLifetime.")]
diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs index 74db74aa..00164835 100644 --- a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -1,11 +1,14 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using System;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Migrations;
using Timeline.Services;
using Xunit;
+using Xunit.Abstractions;
namespace Timeline.Tests.Helpers
{
@@ -35,7 +38,7 @@ namespace Timeline.Tests.Helpers if (_createUser)
{
var passwordService = new PasswordService();
- var userService = new UserService(NullLogger<UserService>.Instance, context, passwordService, new Clock(), new UserPermissionService(context));
+ var userService = new UserService(NullLogger<UserService>.Instance, context, passwordService, new UserPermissionService(context), new Clock());
var admin = await userService.CreateUser("admin", "adminpw");
await userService.ModifyUser(admin.Id, new ModifyUserParams() { Nickname = "administrator" });
@@ -54,12 +57,14 @@ namespace Timeline.Tests.Helpers public SqliteConnection Connection { get; }
- public DatabaseContext CreateContext()
+ public DatabaseContext CreateContext(ITestOutputHelper? testOutputHelper = null)
{
- var options = new DbContextOptionsBuilder<DatabaseContext>()
- .UseSqlite(Connection).Options;
+ var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>()
+ .UseSqlite(Connection);
- return new DatabaseContext(options);
+ if (testOutputHelper != null) optionsBuilder.LogTo(testOutputHelper.WriteLine).EnableDetailedErrors().EnableSensitiveDataLogging();
+
+ return new DatabaseContext(optionsBuilder.Options);
}
}
}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs new file mode 100644 index 00000000..d4b4d55d --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs @@ -0,0 +1,91 @@ +using FluentAssertions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using Xunit;
+
+namespace Timeline.Tests.IntegratedTests
+{
+ public class HighlightTimelineTest : IntegratedTestBase
+ {
+ [Fact]
+ public async Task PermissionTest()
+ {
+ using var client = await CreateClientAsUser();
+
+ await client.TestPutAssertForbiddenAsync("highlights/@user1");
+ await client.TestDeleteAssertForbiddenAsync("highlights/@user1");
+ await client.TestPostAssertForbiddenAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = 1 });
+ }
+
+ [Fact]
+ public async Task InvalidModel()
+ {
+ using var client = await CreateClientAsAdministrator();
+
+ await client.TestPutAssertInvalidModelAsync("highlights/!!!");
+ await client.TestDeleteAssertInvalidModelAsync("highlights/!!!");
+ await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = null!, NewPosition = 1 });
+ await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = null });
+ }
+
+ [Fact]
+ public async Task ShouldWork()
+ {
+ {
+ using var client1 = await CreateClientAsUser();
+ await client1.TestPostAsync("timelines", new TimelineCreateRequest { Name = "t1" });
+ }
+
+ using var client = await CreateClientAsAdministrator();
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().BeEmpty();
+ }
+
+ await client.TestPutAsync("highlights/@user1");
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().HaveCount(1);
+ h[0].Name.Should().Be("@user1");
+ }
+
+ await client.TestPutAsync("highlights/t1");
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().HaveCount(2);
+ h[0].Name.Should().Be("@user1");
+ h[1].Name.Should().Be("t1");
+ }
+
+ await client.TestPostAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "@user1", NewPosition = 2 });
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().HaveCount(2);
+ h[0].Name.Should().Be("t1");
+ h[1].Name.Should().Be("@user1");
+ }
+
+ await client.TestDeleteAsync("highlights/@user1");
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().HaveCount(1);
+ h[0].Name.Should().Be("t1");
+ }
+
+ await client.TestDeleteAsync("highlights/t1");
+
+ {
+ var h = await client.TestGetAsync<List<HttpTimeline>>("highlights");
+ h.Should().BeEmpty();
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs index 7cff0c39..ec517362 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs @@ -212,6 +212,11 @@ namespace Timeline.Tests.IntegratedTests await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Post, url, jsonBody, errorCode, headerSetup);
}
+ public static async Task TestPutAssertForbiddenAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null)
+ {
+ await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Put, url, jsonBody, errorCode, headerSetup);
+ }
+
public static async Task TestPatchAssertForbiddenAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null)
{
await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Patch, url, jsonBody, errorCode, headerSetup);
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs index 8e48ccbf..ac60ce7c 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs @@ -6,11 +6,11 @@ namespace Timeline.Tests.IntegratedTests {
public static class HttpClientTimelineExtensions
{
- public static Task<TimelineInfo> GetTimelineAsync(this HttpClient client, string timelineName)
- => client.TestGetAsync<TimelineInfo>($"timelines/{timelineName}");
+ public static Task<HttpTimeline> GetTimelineAsync(this HttpClient client, string timelineName)
+ => client.TestGetAsync<HttpTimeline>($"timelines/{timelineName}");
- public static Task<TimelineInfo> PatchTimelineAsync(this HttpClient client, string timelineName, TimelinePatchRequest body)
- => client.TestPatchAsync<TimelineInfo>($"timelines/{timelineName}", body);
+ public static Task<HttpTimeline> PatchTimelineAsync(this HttpClient client, string timelineName, HttpTimelinePatchRequest body)
+ => client.TestPatchAsync<HttpTimeline>($"timelines/{timelineName}", body);
public static Task PutTimelineMemberAsync(this HttpClient client, string timelineName, string memberUsername)
=> client.TestPutAsync($"timelines/{timelineName}/members/{memberUsername}");
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs index 81787eef..7ca62e38 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs @@ -6,7 +6,7 @@ namespace Timeline.Tests.IntegratedTests {
public static class HttpClientUserExtensions
{
- public static Task<UserInfo> GetUserAsync(this HttpClient client, string username)
- => client.TestGetAsync<UserInfo>($"users/{username}");
+ public static Task<HttpUser> GetUserAsync(this HttpClient client, string username)
+ => client.TestGetAsync<HttpUser>($"users/{username}");
}
}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index e426ac98..82aed24e 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -103,8 +103,8 @@ namespace Timeline.Tests.IntegratedTests public async Task<HttpClient> CreateClientWithCredential(string username, string password, bool setApiBase = true)
{
var client = await CreateDefaultClient(setApiBase);
- var res = await client.TestPostAsync<CreateTokenResponse>("token/create",
- new CreateTokenRequest { Username = username, Password = password });
+ var res = await client.TestPostAsync<HttpCreateTokenResponse>("token/create",
+ new HttpCreateTokenRequest { Username = username, Password = password });
var token = res.Token;
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
return client;
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs index 9845e1b1..12dd2b8d 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -19,20 +19,20 @@ namespace Timeline.Tests.IntegratedTests {
public static class TimelineHelper
{
- public static TimelinePostContentInfo TextPostContent(string text)
+ public static HttpTimelinePostContent TextPostContent(string text)
{
- return new TimelinePostContentInfo
+ return new HttpTimelinePostContent
{
Type = "text",
Text = text
};
}
- public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null)
+ public static HttpTimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null)
{
- return new TimelinePostCreateRequest
+ return new HttpTimelinePostCreateRequest
{
- Content = new TimelinePostCreateRequestContent
+ Content = new HttpTimelinePostCreateRequestContent
{
Type = "text",
Text = text
@@ -72,7 +72,7 @@ namespace Timeline.Tests.IntegratedTests {
- var body = await client.TestGetAsync<TimelineInfo>("timelines/@user1");
+ var body = await client.TestGetAsync<HttpTimeline>("timelines/@user1");
body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
body.Visibility.Should().Be(TimelineVisibility.Register);
body.Description.Should().Be("");
@@ -84,7 +84,7 @@ namespace Timeline.Tests.IntegratedTests }
{
- var body = await client.TestGetAsync<TimelineInfo>("timelines/t1");
+ var body = await client.TestGetAsync<HttpTimeline>("timelines/t1");
body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
body.Visibility.Should().Be(TimelineVisibility.Register);
body.Description.Should().Be("");
@@ -101,7 +101,7 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateDefaultClient();
- var result = new List<TimelineInfo>
+ var result = new List<HttpTimeline>
{
await client.GetTimelineAsync("@user1")
};
@@ -112,7 +112,7 @@ namespace Timeline.Tests.IntegratedTests }
- var body = await client.TestGetAsync<List<TimelineInfo>>("timelines");
+ var body = await client.TestGetAsync<List<HttpTimeline>>("timelines");
body.Should().BeEquivalentTo(result);
}
@@ -127,14 +127,14 @@ namespace Timeline.Tests.IntegratedTests await client.TestGetAssertInvalidModelAsync("timelines?visibility=aaa");
}
- var testResultRelate = new List<TimelineInfo>();
- var testResultOwn = new List<TimelineInfo>();
- var testResultJoin = new List<TimelineInfo>();
- var testResultOwnPrivate = new List<TimelineInfo>();
- var testResultRelatePublic = new List<TimelineInfo>();
- var testResultRelateRegister = new List<TimelineInfo>();
- var testResultJoinPrivate = new List<TimelineInfo>();
- var testResultPublic = new List<TimelineInfo>();
+ var testResultRelate = new List<HttpTimeline>();
+ var testResultOwn = new List<HttpTimeline>();
+ var testResultJoin = new List<HttpTimeline>();
+ var testResultOwnPrivate = new List<HttpTimeline>();
+ var testResultRelatePublic = new List<HttpTimeline>();
+ var testResultRelateRegister = new List<HttpTimeline>();
+ var testResultJoinPrivate = new List<HttpTimeline>();
+ var testResultPublic = new List<HttpTimeline>();
{
using var client = await CreateClientAsUser();
@@ -185,8 +185,8 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAs(3);
- await client.PatchTimelineAsync("@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private });
- await client.PatchTimelineAsync("t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register });
+ await client.PatchTimelineAsync("@user3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Private });
+ await client.PatchTimelineAsync("t3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Register });
{
var timeline = await client.GetTimelineAsync("@user3");
@@ -206,9 +206,9 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateDefaultClient();
- async Task TestAgainst(string url, List<TimelineInfo> against)
+ async Task TestAgainst(string url, List<HttpTimeline> against)
{
- var body = await client.TestGetAsync<List<TimelineInfo>>(url);
+ var body = await client.TestGetAsync<List<HttpTimeline>>(url);
body.Should().BeEquivalentTo(against);
}
@@ -236,7 +236,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPostAssertInvalidModelAsync("timelines", new TimelineCreateRequest { Name = "!!!" });
{
- var body = await client.TestPostAsync<TimelineInfo>("timelines", new TimelineCreateRequest { Name = "aaa" });
+ var body = await client.TestPostAsync<HttpTimeline>("timelines", new TimelineCreateRequest { Name = "aaa" });
body.Should().BeEquivalentTo(await client.GetTimelineAsync("aaa"));
}
@@ -342,7 +342,7 @@ namespace Timeline.Tests.IntegratedTests var timelineName = generator(1);
- async Task AssertMembers(List<UserInfo> members)
+ async Task AssertMembers(List<HttpUser> members)
{
var body = await client.GetTimelineAsync(timelineName);
body.Members.Should().NotBeNull().And.BeEquivalentTo(members);
@@ -358,7 +358,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPutAssertErrorAsync($"timelines/{timelineName}/members/usernotexist", errorCode: ErrorCodes.TimelineController.MemberPut_NotExist);
await AssertEmptyMembers();
await client.PutTimelineMemberAsync(timelineName, "user2");
- await AssertMembers(new List<UserInfo> { await client.GetUserAsync("user2") });
+ await AssertMembers(new List<HttpUser> { await client.GetUserAsync("user2") });
await client.DeleteTimelineMemberAsync(timelineName, "user2", true);
await AssertEmptyMembers();
await client.DeleteTimelineMemberAsync(timelineName, "aaa", false);
@@ -462,7 +462,7 @@ namespace Timeline.Tests.IntegratedTests async Task<long> CreatePost(int userNumber)
{
using var client = await CreateClientAs(userNumber);
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa"));
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa"));
return body.Id;
}
@@ -515,28 +515,28 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().BeEmpty();
}
const string mockContent = "aaa";
- TimelinePostInfo createRes;
+ HttpTimelinePost createRes;
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent));
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent));
body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent));
body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
body.Deleted.Should().BeFalse();
createRes = body;
}
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().BeEquivalentTo(createRes);
}
const string mockContent2 = "bbb";
var mockTime2 = DateTime.UtcNow.AddDays(-1);
- TimelinePostInfo createRes2;
+ HttpTimelinePost createRes2;
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2));
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2));
body.Should().NotBeNull();
body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2));
body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
@@ -545,7 +545,7 @@ namespace Timeline.Tests.IntegratedTests createRes2 = body;
}
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().BeEquivalentTo(createRes, createRes2);
}
{
@@ -554,7 +554,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/30000", false);
}
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().BeEquivalentTo(createRes2);
}
}
@@ -567,7 +567,7 @@ namespace Timeline.Tests.IntegratedTests async Task<long> CreatePost(DateTime time)
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time));
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time));
return body.Id;
}
@@ -577,7 +577,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now);
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Select(p => p.Id).Should().Equal(id1, id2, id0);
}
}
@@ -588,15 +588,15 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAsUser();
var postUrl = $"timelines/{generator(1)}/posts";
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = null! });
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null! } });
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } });
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } });
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = null! });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = null! } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "hahaha" } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = null } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = null } });
// image not base64
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = "!!!" } });
// image base64 not image
- await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } });
+ await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } });
}
[Theory]
@@ -608,7 +608,7 @@ namespace Timeline.Tests.IntegratedTests long postId;
string postImageUrl;
- void AssertPostContent(TimelinePostContentInfo content)
+ void AssertPostContent(HttpTimelinePostContent content)
{
content.Type.Should().Be(TimelinePostContentTypes.Image);
content.Url.Should().EndWith($"timelines/{generator(1)}/posts/{postId}/data");
@@ -618,10 +618,10 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts",
- new TimelinePostCreateRequest
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
+ new HttpTimelinePostCreateRequest
{
- Content = new TimelinePostCreateRequestContent
+ Content = new HttpTimelinePostCreateRequestContent
{
Type = TimelinePostContentTypes.Image,
Data = Convert.ToBase64String(imageData)
@@ -633,7 +633,7 @@ namespace Timeline.Tests.IntegratedTests }
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().HaveCount(1);
var post = body[0];
post.Id.Should().Be(postId);
@@ -655,7 +655,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}", false);
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts");
body.Should().BeEmpty();
}
@@ -677,7 +677,7 @@ namespace Timeline.Tests.IntegratedTests long postId;
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa"));
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa"));
postId = body.Id;
}
@@ -726,12 +726,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
var postContentList = new List<string> { "a", "b", "c", "d" };
- var posts = new List<TimelinePostInfo>();
+ var posts = new List<HttpTimelinePost>();
foreach (var content in postContentList)
{
- var post = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts",
- new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
+ new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
posts.Add(post);
await Task.Delay(1000);
}
@@ -739,7 +739,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}", true);
{
- var body = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }");
+ var body = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }");
body.Should().HaveCount(2)
.And.Subject.Select(p => p.Content!.Text).Should().Equal("b", "d");
}
@@ -752,12 +752,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
var postContentList = new List<string> { "a", "b", "c", "d" };
- var posts = new List<TimelinePostInfo>();
+ var posts = new List<HttpTimelinePost>();
foreach (var content in postContentList)
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts",
- new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
+ new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
posts.Add(body);
}
@@ -767,7 +767,7 @@ namespace Timeline.Tests.IntegratedTests }
{
- posts = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts?includeDeleted=true");
+ posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?includeDeleted=true");
posts.Should().HaveCount(4);
posts.Select(p => p.Deleted).Should().Equal(true, false, true, false);
posts.Select(p => p.Content == null).Should().Equal(true, false, true, false);
@@ -781,12 +781,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
var postContentList = new List<string> { "a", "b", "c", "d" };
- var posts = new List<TimelinePostInfo>();
+ var posts = new List<HttpTimelinePost>();
foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
{
- var post = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts",
- new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
+ var post = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts",
+ new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } });
posts.Add(post);
await Task.Delay(1000);
}
@@ -795,7 +795,7 @@ namespace Timeline.Tests.IntegratedTests {
- posts = await client.TestGetAsync<List<TimelinePostInfo>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true");
+ posts = await client.TestGetAsync<List<HttpTimelinePost>>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true");
posts.Should().HaveCount(3);
posts.Select(p => p.Deleted).Should().Equal(false, true, false);
posts.Select(p => p.Content == null).Should().Equal(false, true, false);
@@ -809,7 +809,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser();
DateTime lastModifiedTime;
- TimelineInfo timeline;
+ HttpTimeline timeline;
string uniqueId;
{
@@ -830,7 +830,7 @@ namespace Timeline.Tests.IntegratedTests {
- var body = await client.TestGetAsync<TimelineInfo>($"timelines/{generator(1)}",
+ var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}",
headerSetup: (headers, _) =>
{
headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1);
@@ -843,7 +843,7 @@ namespace Timeline.Tests.IntegratedTests }
{
- var body = await client.TestGetAsync<TimelineInfo>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }");
+ var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }");
body.Should().BeEquivalentTo(timeline);
}
@@ -853,7 +853,7 @@ namespace Timeline.Tests.IntegratedTests {
var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..];
- var body = await client.TestGetAsync<TimelineInfo>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}");
+ var body = await client.TestGetAsync<HttpTimeline>($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}");
body.Should().BeEquivalentTo(timeline);
}
}
@@ -870,7 +870,7 @@ namespace Timeline.Tests.IntegratedTests }
{
- var body = await client.PatchTimelineAsync(generator(1), new TimelinePatchRequest { Title = "atitle" });
+ var body = await client.PatchTimelineAsync(generator(1), new HttpTimelinePatchRequest { Title = "atitle" });
body.Title.Should().Be("atitle");
}
@@ -885,26 +885,26 @@ namespace Timeline.Tests.IntegratedTests {
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
}
{
using var client = await CreateClientAs(2);
- await client.TestPostAssertForbiddenAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
+ await client.TestPostAssertForbiddenAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" });
}
using (var client = await CreateClientAsUser())
{
- await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" });
- await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" });
- await client.TestPostAssertErrorAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist);
+ await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" });
+ await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" });
+ await client.TestPostAssertErrorAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist);
- await client.TestPostAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" });
+ await client.TestPostAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "newt" });
await client.TestGetAsync("timelines/t1", expectedStatusCode: HttpStatusCode.NotFound);
{
- var body = await client.TestGetAsync<TimelineInfo>("timelines/newt");
+ var body = await client.TestGetAsync<HttpTimeline>("timelines/newt");
body.Name.Should().Be("newt");
}
}
@@ -920,9 +920,9 @@ namespace Timeline.Tests.IntegratedTests string etag;
{
- var body = await client.TestPostAsync<TimelinePostInfo>($"timelines/{generator(1)}/posts", new TimelinePostCreateRequest
+ var body = await client.TestPostAsync<HttpTimelinePost>($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest
{
- Content = new TimelinePostCreateRequestContent
+ Content = new HttpTimelinePostCreateRequestContent
{
Type = TimelinePostContentTypes.Image,
Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50))
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs index a5208618..fdf1af99 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -14,9 +14,9 @@ namespace Timeline.Tests.IntegratedTests private const string CreateTokenUrl = "token/create";
private const string VerifyTokenUrl = "token/verify";
- private static async Task<CreateTokenResponse> CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null)
+ private static async Task<HttpCreateTokenResponse> CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null)
{
- return await client.TestPostAsync<CreateTokenResponse>(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset });
+ return await client.TestPostAsync<HttpCreateTokenResponse>(CreateTokenUrl, new HttpCreateTokenRequest { Username = username, Password = password, Expire = expireOffset });
}
public static IEnumerable<object?[]> CreateToken_InvalidModel_Data()
@@ -32,7 +32,7 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_InvalidModel(string username, string password, int expire)
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new CreateTokenRequest
+ await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new HttpCreateTokenRequest
{
Username = username,
Password = password,
@@ -52,7 +52,7 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateDefaultClient();
await client.TestPostAssertErrorAsync(CreateTokenUrl,
- new CreateTokenRequest { Username = username, Password = password },
+ new HttpCreateTokenRequest { Username = username, Password = password },
errorCode: ErrorCodes.TokenController.Create_BadCredential);
}
@@ -60,8 +60,8 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_Success()
{
using var client = await CreateDefaultClient();
- var body = await client.TestPostAsync<CreateTokenResponse>(CreateTokenUrl,
- new CreateTokenRequest { Username = "user1", Password = "user1pw" });
+ var body = await client.TestPostAsync<HttpCreateTokenResponse>(CreateTokenUrl,
+ new HttpCreateTokenRequest { Username = "user1", Password = "user1pw" });
body.Token.Should().NotBeNullOrWhiteSpace();
body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
}
@@ -70,7 +70,7 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_InvalidModel()
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = null! });
+ await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new HttpVerifyTokenRequest { Token = null! });
}
[Fact]
@@ -78,7 +78,7 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateDefaultClient();
await client.TestPostAssertErrorAsync(VerifyTokenUrl,
- new VerifyTokenRequest { Token = "bad token hahaha" },
+ new HttpVerifyTokenRequest { Token = "bad token hahaha" },
errorCode: ErrorCodes.TokenController.Verify_BadFormat);
}
@@ -97,7 +97,7 @@ namespace Timeline.Tests.IntegratedTests }
await client.TestPostAssertErrorAsync(VerifyTokenUrl,
- new VerifyTokenRequest { Token = token },
+ new HttpVerifyTokenRequest { Token = token },
errorCode: ErrorCodes.TokenController.Verify_OldVersion);
}
@@ -114,7 +114,7 @@ namespace Timeline.Tests.IntegratedTests }
await client.TestPostAssertErrorAsync(VerifyTokenUrl,
- new VerifyTokenRequest { Token = token },
+ new HttpVerifyTokenRequest { Token = token },
errorCode: ErrorCodes.TokenController.Verify_UserNotExist);
}
@@ -141,8 +141,8 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateDefaultClient();
var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw");
- var body = await client.TestPostAsync<VerifyTokenResponse>(VerifyTokenUrl,
- new VerifyTokenRequest { Token = createTokenResult.Token });
+ var body = await client.TestPostAsync<HttpVerifyTokenResponse>(VerifyTokenUrl,
+ new HttpVerifyTokenRequest { Token = createTokenResult.Token });
body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1"));
}
}
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs index e0ebf635..56dbf92a 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.IntegratedTests public async Task UserListShouldHaveUniqueId()
{
using var client = await CreateDefaultClient();
- foreach (var user in await client.TestGetAsync<List<UserInfo>>("users"))
+ foreach (var user in await client.TestGetAsync<List<HttpUser>>("users"))
{
user.UniqueId.Should().NotBeNullOrWhiteSpace();
}
@@ -22,14 +22,14 @@ namespace Timeline.Tests.IntegratedTests public async Task GetList()
{
using var client = await CreateDefaultClient();
- await client.TestGetAsync<List<UserInfo>>("users");
+ await client.TestGetAsync<List<HttpUser>>("users");
}
[Fact]
public async Task Get()
{
using var client = await CreateDefaultClient();
- var user = await client.TestGetAsync<UserInfo>($"users/admin");
+ var user = await client.TestGetAsync<HttpUser>($"users/admin");
user.Username.Should().Be("admin");
user.Nickname.Should().Be("administrator");
user.UniqueId.Should().NotBeNullOrEmpty();
@@ -55,8 +55,8 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAsUser();
{
- var body = await client.TestPatchAsync<UserInfo>("users/user1",
- new UserPatchRequest { Nickname = "aaa" });
+ var body = await client.TestPatchAsync<HttpUser>("users/user1",
+ new HttpUserPatchRequest { Nickname = "aaa" });
body.Nickname.Should().Be("aaa");
}
@@ -73,8 +73,8 @@ namespace Timeline.Tests.IntegratedTests using var userClient = await CreateClientAsUser();
{
- var body = await client.TestPatchAsync<UserInfo>("users/user1",
- new UserPatchRequest
+ var body = await client.TestPatchAsync<HttpUser>("users/user1",
+ new HttpUserPatchRequest
{
Username = "newuser",
Password = "newpw",
@@ -91,7 +91,7 @@ namespace Timeline.Tests.IntegratedTests {
var token = userClient.DefaultRequestHeaders.Authorization!.Parameter!;
// Token should expire.
- await userClient.TestPostAssertErrorAsync("token/verify", new VerifyTokenRequest() { Token = token });
+ await userClient.TestPostAssertErrorAsync("token/verify", new HttpVerifyTokenRequest() { Token = token });
}
{
@@ -104,26 +104,26 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_NotExist()
{
using var client = await CreateClientAsAdministrator();
- await client.TestPatchAssertNotFoundAsync("users/usernotexist", new UserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist);
+ await client.TestPatchAssertNotFoundAsync("users/usernotexist", new HttpUserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist);
}
[Fact]
public async Task Patch_InvalidModel()
{
using var client = await CreateClientAsAdministrator();
- await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new UserPatchRequest { });
+ await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new HttpUserPatchRequest { });
}
public static IEnumerable<object[]> Patch_InvalidModel_Body_Data()
{
- yield return new[] { new UserPatchRequest { Username = "aaa!a" } };
- yield return new[] { new UserPatchRequest { Password = "" } };
- yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } };
+ yield return new[] { new HttpUserPatchRequest { Username = "aaa!a" } };
+ yield return new[] { new HttpUserPatchRequest { Password = "" } };
+ yield return new[] { new HttpUserPatchRequest { Nickname = new string('a', 50) } };
}
[Theory]
[MemberData(nameof(Patch_InvalidModel_Body_Data))]
- public async Task Patch_InvalidModel_Body(UserPatchRequest body)
+ public async Task Patch_InvalidModel_Body(HttpUserPatchRequest body)
{
using var client = await CreateClientAsAdministrator();
await client.TestPatchAssertInvalidModelAsync("users/user1", body);
@@ -133,35 +133,35 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_UsernameConflict()
{
using var client = await CreateClientAsAdministrator();
- await client.TestPatchAssertErrorAsync("users/user1", new UserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict);
+ await client.TestPatchAssertErrorAsync("users/user1", new HttpUserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict);
}
[Fact]
public async Task Patch_NoAuth_Unauthorized()
{
using var client = await CreateDefaultClient();
- await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { Nickname = "aaa" });
+ await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { Nickname = "aaa" });
}
[Fact]
public async Task Patch_User_Forbid()
{
using var client = await CreateClientAsUser();
- await client.TestPatchAssertForbiddenAsync("users/admin", new UserPatchRequest { Nickname = "aaa" });
+ await client.TestPatchAssertForbiddenAsync("users/admin", new HttpUserPatchRequest { Nickname = "aaa" });
}
[Fact]
public async Task Patch_Username_Forbid()
{
using var client = await CreateClientAsUser();
- await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Username = "aaa" });
+ await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Username = "aaa" });
}
[Fact]
public async Task Patch_Password_Forbid()
{
using var client = await CreateClientAsUser();
- await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Password = "aaa" });
+ await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Password = "aaa" });
}
[Fact]
@@ -214,7 +214,7 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAsAdministrator();
{
- var body = await client.TestPostAsync<UserInfo>(createUserUrl, new CreateUserRequest
+ var body = await client.TestPostAsync<HttpUser>(createUserUrl, new HttpCreateUserRequest
{
Username = "aaa",
Password = "bbb",
@@ -233,15 +233,15 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable<object[]> Op_CreateUser_InvalidModel_Data()
{
- yield return new[] { new CreateUserRequest { Username = "aaa" } };
- yield return new[] { new CreateUserRequest { Password = "bbb" } };
- yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb" } };
- yield return new[] { new CreateUserRequest { Username = "aaa", Password = "" } };
+ yield return new[] { new HttpCreateUserRequest { Username = "aaa" } };
+ yield return new[] { new HttpCreateUserRequest { Password = "bbb" } };
+ yield return new[] { new HttpCreateUserRequest { Username = "a!a", Password = "bbb" } };
+ yield return new[] { new HttpCreateUserRequest { Username = "aaa", Password = "" } };
}
[Theory]
[MemberData(nameof(Op_CreateUser_InvalidModel_Data))]
- public async Task Op_CreateUser_InvalidModel(CreateUserRequest body)
+ public async Task Op_CreateUser_InvalidModel(HttpCreateUserRequest body)
{
using var client = await CreateClientAsAdministrator();
await client.TestPostAssertInvalidModelAsync(createUserUrl, body);
@@ -251,7 +251,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_UsernameConflict()
{
using var client = await CreateClientAsAdministrator();
- await client.TestPostAssertErrorAsync(createUserUrl, new CreateUserRequest
+ await client.TestPostAssertErrorAsync(createUserUrl, new HttpCreateUserRequest
{
Username = "user1",
Password = "bbb",
@@ -262,7 +262,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_NoAuth_Unauthorized()
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertUnauthorizedAsync(createUserUrl, new CreateUserRequest
+ await client.TestPostAssertUnauthorizedAsync(createUserUrl, new HttpCreateUserRequest
{
Username = "aaa",
Password = "bbb",
@@ -273,7 +273,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_User_Forbid()
{
using var client = await CreateClientAsUser();
- await client.TestPostAssertForbiddenAsync(createUserUrl, new CreateUserRequest
+ await client.TestPostAssertForbiddenAsync(createUserUrl, new HttpCreateUserRequest
{
Username = "aaa",
Password = "bbb",
@@ -286,8 +286,8 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_ChangePassword()
{
using var client = await CreateClientAsUser();
- await client.TestPostAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" });
- await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { });
+ await client.TestPostAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" });
+ await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { });
(await CreateClientWithCredential("user1", "newpw")).Dispose();
}
@@ -303,21 +303,21 @@ namespace Timeline.Tests.IntegratedTests {
using var client = await CreateClientAsUser();
await client.TestPostAssertInvalidModelAsync(changePasswordUrl,
- new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
+ new HttpChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword });
}
[Fact]
public async Task Op_ChangePassword_BadOldPassword()
{
using var client = await CreateClientAsUser();
- await client.TestPostAssertErrorAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword);
+ await client.TestPostAssertErrorAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword);
}
[Fact]
public async Task Op_ChangePassword_NoAuth_Unauthorized()
{
using var client = await CreateDefaultClient();
- await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
+ await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" });
}
}
}
diff --git a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs b/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs index 3bb6ebb5..90fb6463 100644 --- a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs +++ b/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs @@ -2,6 +2,7 @@ using Timeline.Entities;
using Timeline.Tests.Helpers;
using Xunit;
+using Xunit.Abstractions;
namespace Timeline.Tests.Services
{
@@ -10,15 +11,20 @@ namespace Timeline.Tests.Services protected TestDatabase TestDatabase { get; }
protected DatabaseContext Database { get; private set; } = default!;
- protected DatabaseBasedTest(bool databaseCreateUsers = true)
+ private readonly ITestOutputHelper? _testOutputHelper;
+
+ protected DatabaseBasedTest(bool databaseCreateUsers = true, ITestOutputHelper? testOutputHelper = null)
{
+ _testOutputHelper = testOutputHelper;
TestDatabase = new TestDatabase(databaseCreateUsers);
}
+ protected DatabaseBasedTest(ITestOutputHelper? testOutputHelper) : this(true, testOutputHelper) { }
+
public async Task InitializeAsync()
{
await TestDatabase.InitializeAsync();
- Database = TestDatabase.CreateContext();
+ Database = TestDatabase.CreateContext(_testOutputHelper);
await OnDatabaseCreatedAsync();
OnDatabaseCreated();
}
diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs new file mode 100644 index 00000000..dca070c6 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -0,0 +1,92 @@ +using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using System.Threading.Tasks;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests.Services
+{
+ public class HighlightTimelineServiceTest : DatabaseBasedTest
+ {
+ private readonly TestClock _clock = new TestClock();
+ private UserService _userService = default!;
+ private TimelineService _timelineService = default!;
+
+ private HighlightTimelineService _service = default!;
+
+ public HighlightTimelineServiceTest(ITestOutputHelper testOutputHelper)
+ : base(testOutputHelper)
+ {
+
+ }
+
+ protected override void OnDatabaseCreated()
+ {
+ _userService = new UserService(NullLogger<UserService>.Instance, Database, new PasswordService(), new UserPermissionService(Database), _clock);
+ _timelineService = new TimelineService(Database, _userService, _clock);
+ _service = new HighlightTimelineService(Database, _userService, _timelineService, _clock);
+ }
+
+ [Fact]
+ public async Task Should_Work()
+ {
+ {
+ var ht = await _service.GetHighlightTimelines();
+ ht.Should().BeEmpty();
+ }
+
+ var userId = await _userService.GetUserIdByUsername("user");
+ await _timelineService.CreateTimeline("tl", userId);
+ await _service.AddHighlightTimeline("tl", userId);
+
+ {
+ var ht = await _service.GetHighlightTimelines();
+ ht.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl"));
+ }
+ }
+
+ [Fact]
+ public async Task NewOne_Should_BeAtLast()
+ {
+ var userId = await _userService.GetUserIdByUsername("user");
+ await _timelineService.CreateTimeline("t1", userId);
+ await _service.AddHighlightTimeline("t1", userId);
+
+ await _timelineService.CreateTimeline("t2", userId);
+ await _service.AddHighlightTimeline("t2", userId);
+
+ var ht = await _service.GetHighlightTimelines();
+
+ ht.Should().HaveCount(2);
+ ht[0].Name.Should().Be("t1");
+ ht[1].Name.Should().Be("t2");
+ }
+
+ [Fact]
+ public async Task Multiple_Should_Work()
+ {
+ var userId = await _userService.GetUserIdByUsername("user");
+ await _timelineService.CreateTimeline("t1", userId);
+ await _service.AddHighlightTimeline("t1", userId);
+
+ await _timelineService.CreateTimeline("t2", userId);
+ await _service.AddHighlightTimeline("t2", userId);
+
+ await _timelineService.CreateTimeline("t3", userId);
+ await _service.AddHighlightTimeline("t3", userId);
+
+ await _service.MoveHighlightTimeline("t3", 2);
+ (await _service.GetHighlightTimelines())[1].Name.Should().Be("t3");
+
+ await _service.MoveHighlightTimeline("t1", 3);
+ (await _service.GetHighlightTimelines())[2].Name.Should().Be("t1");
+
+ await _service.RemoveHighlightTimeline("t2", userId);
+ await _service.RemoveHighlightTimeline("t1", userId);
+ await _service.RemoveHighlightTimeline("t3", userId);
+ (await _service.GetHighlightTimelines()).Should().BeEmpty();
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs new file mode 100644 index 00000000..7771ae0b --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -0,0 +1,200 @@ +using FluentAssertions;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.Services
+{
+ public class TimelinePostServiceTest : DatabaseBasedTest
+ {
+ private readonly PasswordService _passwordService = new PasswordService();
+
+ private readonly ETagGenerator _eTagGenerator = new ETagGenerator();
+
+ private readonly ImageValidator _imageValidator = new ImageValidator();
+
+ private readonly TestClock _clock = new TestClock();
+
+ private DataManager _dataManager = default!;
+
+ private UserPermissionService _userPermissionService = default!;
+
+ private UserService _userService = default!;
+
+ private TimelineService _timelineService = default!;
+
+ private TimelinePostService _timelinePostService = default!;
+
+ private UserDeleteService _userDeleteService = default!;
+
+ protected override void OnDatabaseCreated()
+ {
+ _dataManager = new DataManager(Database, _eTagGenerator);
+ _userPermissionService = new UserPermissionService(Database);
+ _userService = new UserService(NullLogger<UserService>.Instance, Database, _passwordService, _userPermissionService, _clock);
+ _timelineService = new TimelineService(Database, _userService, _clock);
+ _timelinePostService = new TimelinePostService(NullLogger<TimelinePostService>.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock);
+ _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _timelinePostService);
+ }
+
+ protected override void BeforeDatabaseDestroy()
+ {
+ _eTagGenerator.Dispose();
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince(string timelineName)
+ {
+ _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ DateTime testPoint = new DateTime();
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ var t = _clock.ForwardCurrentTime();
+ if (index == 1)
+ testPoint = t;
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelinePostService.GetPosts(timelineName, testPoint);
+ posts.Should().HaveCount(3)
+ .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1));
+ }
+
+ [Theory]
+ [InlineData("@user")]
+ [InlineData("tl")]
+ public async Task GetPosts_IncludeDeleted(string timelineName)
+ {
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var content in postContentList)
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var posts = await _timelinePostService.GetPosts(timelineName);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList);
+
+ foreach (var id in new long[] { posts[0].Id, posts[2].Id })
+ {
+ await _timelinePostService.DeletePost(timelineName, id);
+ }
+
+ posts = await _timelinePostService.GetPosts(timelineName);
+ posts.Should().HaveCount(2);
+ posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
+ posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
+
+ posts = await _timelinePostService.GetPosts(timelineName, includeDeleted: true);
+ posts.Should().HaveCount(4);
+ posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false });
+ posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, userId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" });
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" });
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(4);
+ }
+ }
+
+ [Theory]
+ [InlineData("@admin")]
+ [InlineData("tl")]
+ public async Task GetPosts_ModifiedSince_UserDelete(string timelineName)
+ {
+ var time1 = _clock.ForwardCurrentTime();
+
+ var userId = await _userService.GetUserIdByUsername("user");
+ var adminId = await _userService.GetUserIdByUsername("admin");
+
+ var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+ if (!isPersonal)
+ await _timelineService.CreateTimeline(timelineName, adminId);
+
+ var postContentList = new string[] { "a", "b", "c", "d" };
+
+ foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
+ {
+ await _timelinePostService.CreateTextPost(timelineName, userId, content, null);
+ }
+
+ var time2 = _clock.ForwardCurrentTime();
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ await _userDeleteService.DeleteUser("user");
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2);
+ posts.Should().HaveCount(0);
+ }
+
+ {
+ var posts = await _timelinePostService.GetPosts(timelineName, time2, true);
+ posts.Should().HaveCount(4);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 5f2c20e8..70f54ede 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Timeline.Models;
using Timeline.Services;
@@ -12,38 +11,23 @@ using Xunit; namespace Timeline.Tests.Services
{
- public class TimelineServiceTest : DatabaseBasedTest, IDisposable
+ public class TimelineServiceTest : DatabaseBasedTest
{
private readonly PasswordService _passwordService = new PasswordService();
- private readonly ETagGenerator _eTagGenerator = new ETagGenerator();
-
- private readonly ImageValidator _imageValidator = new ImageValidator();
-
private readonly TestClock _clock = new TestClock();
- private DataManager _dataManager = default!;
-
private UserPermissionService _userPermissionService = default!;
private UserService _userService = default!;
private TimelineService _timelineService = default!;
- private UserDeleteService _userDeleteService = default!;
-
protected override void OnDatabaseCreated()
{
- _dataManager = new DataManager(Database, _eTagGenerator);
_userPermissionService = new UserPermissionService(Database);
- _userService = new UserService(NullLogger<UserService>.Instance, Database, _passwordService, _clock, _userPermissionService);
- _timelineService = new TimelineService(NullLogger<TimelineService>.Instance, Database, _dataManager, _userService, _imageValidator, _clock);
- _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _timelineService);
- }
-
- public void Dispose()
- {
- _eTagGenerator.Dispose();
+ _userService = new UserService(NullLogger<UserService>.Instance, Database, _passwordService, _userPermissionService, _clock);
+ _timelineService = new TimelineService(Database, _userService, _clock);
}
[Theory]
@@ -83,7 +67,7 @@ namespace Timeline.Tests.Services {
var initTime = _clock.ForwardCurrentTime();
- void Check(Models.Timeline timeline)
+ void Check(TimelineInfo timeline)
{
timeline.NameLastModified.Should().Be(initTime);
timeline.LastModified.Should().Be(_clock.GetCurrentTime());
@@ -110,156 +94,6 @@ namespace Timeline.Tests.Services }
[Theory]
- [InlineData("@user")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince(string timelineName)
- {
- _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- DateTime testPoint = new DateTime();
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- var t = _clock.ForwardCurrentTime();
- if (index == 1)
- testPoint = t;
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var posts = await _timelineService.GetPosts(timelineName, testPoint);
- posts.Should().HaveCount(3)
- .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1));
- }
-
- [Theory]
- [InlineData("@user")]
- [InlineData("tl")]
- public async Task GetPosts_IncludeDeleted(string timelineName)
- {
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var content in postContentList)
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var posts = await _timelineService.GetPosts(timelineName);
- posts.Should().HaveCount(4);
- posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
- posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList);
-
- foreach (var id in new long[] { posts[0].Id, posts[2].Id })
- {
- await _timelineService.DeletePost(timelineName, id);
- }
-
- posts = await _timelineService.GetPosts(timelineName);
- posts.Should().HaveCount(2);
- posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count));
- posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
-
- posts = await _timelineService.GetPosts(timelineName, includeDeleted: true);
- posts.Should().HaveCount(4);
- posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false });
- posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" });
- }
-
- [Theory]
- [InlineData("@admin")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName)
- {
- var time1 = _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, userId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var time2 = _clock.ForwardCurrentTime();
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" });
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" });
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(4);
- }
- }
-
- [Theory]
- [InlineData("@admin")]
- [InlineData("tl")]
- public async Task GetPosts_ModifiedSince_UserDelete(string timelineName)
- {
- var time1 = _clock.ForwardCurrentTime();
-
- var userId = await _userService.GetUserIdByUsername("user");
- var adminId = await _userService.GetUserIdByUsername("admin");
-
- var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
- if (!isPersonal)
- await _timelineService.CreateTimeline(timelineName, adminId);
-
- var postContentList = new string[] { "a", "b", "c", "d" };
-
- foreach (var (content, index) in postContentList.Select((v, i) => (v, i)))
- {
- await _timelineService.CreateTextPost(timelineName, userId, content, null);
- }
-
- var time2 = _clock.ForwardCurrentTime();
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- await _userDeleteService.DeleteUser("user");
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2);
- posts.Should().HaveCount(0);
- }
-
- {
- var posts = await _timelineService.GetPosts(timelineName, time2, true);
- posts.Should().HaveCount(4);
- }
- }
-
- [Theory]
[InlineData("@admin")]
[InlineData("tl")]
public async Task Title(string timelineName)
diff --git a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs index be11564e..59c0a9af 100644 --- a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs @@ -10,12 +10,12 @@ namespace Timeline.Tests.Services {
public class UserDeleteServiceTest : DatabaseBasedTest
{
- private readonly Mock<ITimelineService> _mockTimelineService = new Mock<ITimelineService>();
+ private readonly Mock<ITimelinePostService> _mockTimelinePostService = new Mock<ITimelinePostService>();
private UserDeleteService _service = default!;
protected override void OnDatabaseCreated()
{
- _service = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _mockTimelineService.Object);
+ _service = new UserDeleteService(NullLogger<UserDeleteService>.Instance, Database, _mockTimelinePostService.Object);
}
[Fact]
diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json index 7d84213e..50b90c3c 100644 --- a/BackEnd/Timeline.Tests/packages.lock.json +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -763,8 +763,8 @@ },
"NJsonSchema": {
"type": "Transitive",
- "resolved": "10.2.1",
- "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==",
+ "resolved": "10.3.1",
+ "contentHash": "k5ptrRSxMy1lZXxU7dXW2Gy9Q7uPufSGtb609tfuFdo+w45UMHdBolvbWeEq482BPXhYfoBZ2uNzjJgcny2o3g==",
"dependencies": {
"Namotion.Reflection": "1.0.14",
"Newtonsoft.Json": "9.0.1"
@@ -772,56 +772,56 @@ },
"NSwag.Annotations": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q=="
+ "resolved": "13.9.4",
+ "contentHash": "qsOYnNMUJJ5VpgYmQsyNkDKbJnMaRo4lGBgkaBlZsHsWGG+HizNkx+HuHkRtI0ks28jqZXpVDxDmnuyq/SwFnw=="
},
"NSwag.AspNetCore": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==",
+ "resolved": "13.9.4",
+ "contentHash": "2+QqWsUMfwOy1pFsacA4hjVW0tmKrcGG4O1JCVhM93q7QfqbG/ndt002a/GGGyaMsuK7HRKmYJ8nmo5tzOE1tg==",
"dependencies": {
"Microsoft.AspNetCore.Mvc.Core": "1.0.4",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
"Microsoft.AspNetCore.StaticFiles": "1.0.4",
"Microsoft.Extensions.ApiDescription.Server": "3.0.0",
"Microsoft.Extensions.FileProviders.Embedded": "1.0.1",
- "NSwag.Annotations": "13.8.2",
- "NSwag.Core": "13.8.2",
- "NSwag.Generation": "13.8.2",
- "NSwag.Generation.AspNetCore": "13.8.2",
+ "NSwag.Annotations": "13.9.4",
+ "NSwag.Core": "13.9.4",
+ "NSwag.Generation": "13.9.4",
+ "NSwag.Generation.AspNetCore": "13.9.4",
"System.IO.FileSystem": "4.3.0",
"System.Xml.XPath.XDocument": "4.0.1"
}
},
"NSwag.Core": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==",
+ "resolved": "13.9.4",
+ "contentHash": "iNhgBGWT5yEYL3uV6Xhla+VspVaN3NfDi+rjDugWLErU+A7uxV71D1i9OQkW37rdOFKiaixJAXPaLA6JA8c8hw==",
"dependencies": {
- "NJsonSchema": "10.2.1",
+ "NJsonSchema": "10.3.1",
"Newtonsoft.Json": "9.0.1"
}
},
"NSwag.Generation": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==",
+ "resolved": "13.9.4",
+ "contentHash": "Y6qqOYUEoYZRL5nYshzYn0b7Nz9Rzr6qCdVkah6mQq39Pom/XQgygaV6JR3t0dacDYg/XmVsMn++bdLPQs9rAw==",
"dependencies": {
- "NJsonSchema": "10.2.1",
- "NSwag.Core": "13.8.2",
+ "NJsonSchema": "10.3.1",
+ "NSwag.Core": "13.9.4",
"Newtonsoft.Json": "9.0.1"
}
},
"NSwag.Generation.AspNetCore": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==",
+ "resolved": "13.9.4",
+ "contentHash": "N0HGoPJsK67GAtNnPln0MLPnmv9wVp9Ev5sfEuWQIa/VHMkXQL6IyvItXiigDLb8VHXnqUbggU7WBg9Ay6h8oQ==",
"dependencies": {
"Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4",
"Microsoft.AspNetCore.Mvc.Core": "1.0.4",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
- "NJsonSchema": "10.2.1",
- "NSwag.Generation": "13.8.2"
+ "NJsonSchema": "10.3.1",
+ "NSwag.Generation": "13.9.4"
}
},
"NuGet.Frameworks": {
@@ -938,8 +938,8 @@ },
"SixLabors.ImageSharp": {
"type": "Transitive",
- "resolved": "1.0.1",
- "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ=="
+ "resolved": "1.0.2",
+ "contentHash": "iZJ37Iss3pUkFl961x1aka85QuvgY9oNZabHijzVnHs4QTz6EMNx3zjJDyvK/0+Ryj6JPv/PC7GVIJXLHtu2nQ=="
},
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
@@ -1985,8 +1985,8 @@ "Microsoft.EntityFrameworkCore": "5.0.0",
"Microsoft.EntityFrameworkCore.Analyzers": "5.0.0",
"Microsoft.EntityFrameworkCore.Sqlite": "5.0.0",
- "NSwag.AspNetCore": "13.8.2",
- "SixLabors.ImageSharp": "1.0.1",
+ "NSwag.AspNetCore": "13.9.4",
+ "SixLabors.ImageSharp": "1.0.2",
"System.IdentityModel.Tokens.Jwt": "6.8.0",
"Timeline.ErrorCodes": "1.0.0"
}
diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs new file mode 100644 index 00000000..0b6e1665 --- /dev/null +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -0,0 +1,107 @@ +using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Timeline.Auth;
+using Timeline.Models.Http;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Controllers
+{
+ /// <summary>
+ /// Api related to highlight timeline.
+ /// </summary>
+ [ApiController]
+ [ProducesErrorResponseType(typeof(CommonResponse))]
+ public class HighlightTimelineController : Controller
+ {
+ private readonly IHighlightTimelineService _service;
+ private readonly IMapper _mapper;
+
+ public HighlightTimelineController(IHighlightTimelineService service, IMapper mapper)
+ {
+ _service = service;
+ _mapper = mapper;
+ }
+
+ /// <summary>
+ /// Get all highlight timelines.
+ /// </summary>
+ /// <returns>Highlight timeline list.</returns>
+ [HttpGet("highlights")]
+ [ProducesResponseType(200)]
+ public async Task<ActionResult<List<HttpTimeline>>> List()
+ {
+ var t = await _service.GetHighlightTimelines();
+ return _mapper.Map<List<HttpTimeline>>(t);
+ }
+
+ /// <summary>
+ /// Add a timeline to highlight list.
+ /// </summary>
+ /// <param name="timeline">The timeline name.</param>
+ [HttpPut("highlights/{timeline}")]
+ [PermissionAuthorize(UserPermission.HighlightTimelineManagement)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ public async Task<ActionResult> Put([GeneralTimelineName] string timeline)
+ {
+ try
+ {
+ await _service.AddHighlightTimeline(timeline, this.GetUserId());
+ return Ok();
+ }
+ catch (TimelineNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.NotExist());
+ }
+ }
+
+ /// <summary>
+ /// Remove a timeline from highlight list.
+ /// </summary>
+ /// <param name="timeline">Timeline name.</param>
+ [HttpDelete("highlights/{timeline}")]
+ [PermissionAuthorize(UserPermission.HighlightTimelineManagement)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ public async Task<ActionResult> Delete([GeneralTimelineName] string timeline)
+ {
+ try
+ {
+ await _service.RemoveHighlightTimeline(timeline, this.GetUserId());
+ return Ok();
+ }
+ catch (TimelineNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.NotExist());
+ }
+ }
+
+ /// <summary>
+ /// Move a highlight position.
+ /// </summary>
+ [HttpPost("highlightop/move")]
+ [PermissionAuthorize(UserPermission.HighlightTimelineManagement)]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(400)]
+ public async Task<ActionResult> Move([FromBody] HttpHighlightTimelineMoveRequest body)
+ {
+ try
+ {
+ await _service.MoveHighlightTimeline(body.Timeline, body.NewPosition!.Value);
+ return Ok();
+ }
+ catch (TimelineNotExistException)
+ {
+ return BadRequest(ErrorResponse.TimelineController.NotExist());
+ }
+ catch (InvalidHighlightTimelineException)
+ {
+ return BadRequest(new CommonResponse(ErrorCodes.HighlightTimelineController.NonHighlight, "Can't move a non-highlight timeline."));
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 45060b5d..27b4b7a7 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -29,17 +29,19 @@ namespace Timeline.Controllers private readonly IUserService _userService;
private readonly ITimelineService _service;
+ private readonly ITimelinePostService _postService;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
- public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, IMapper mapper)
+ public TimelineController(ILogger<TimelineController> logger, IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, IMapper mapper)
{
_logger = logger;
_userService = userService;
_service = service;
+ _postService = timelinePostService;
_mapper = mapper;
}
@@ -55,7 +57,7 @@ namespace Timeline.Controllers [HttpGet("timelines")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult<List<TimelineInfo>>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility)
+ public async Task<ActionResult<List<HttpTimeline>>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility)
{
List<TimelineVisibility>? visibilityFilter = null;
if (visibility != null)
@@ -107,7 +109,7 @@ namespace Timeline.Controllers }
var timelines = await _service.GetTimelines(relationship, visibilityFilter);
- var result = _mapper.Map<List<TimelineInfo>>(timelines);
+ var result = _mapper.Map<List<HttpTimeline>>(timelines);
return result;
}
@@ -123,7 +125,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status304NotModified)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<TimelineInfo>> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince)
+ public async Task<ActionResult<HttpTimeline>> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince)
{
DateTime? ifModifiedSince = null;
if (queryIfModifiedSince.HasValue)
@@ -164,7 +166,7 @@ namespace Timeline.Controllers else
{
var timeline = await _service.GetTimeline(name);
- var result = _mapper.Map<TimelineInfo>(timeline);
+ var result = _mapper.Map<HttpTimeline>(timeline);
return result;
}
}
@@ -180,16 +182,16 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<List<TimelinePostInfo>>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
+ public async Task<ActionResult<List<HttpTimelinePost>>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted)
{
if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(name, this.GetOptionalUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
- List<TimelinePost> posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false);
+ List<TimelinePostInfo> posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false);
- var result = _mapper.Map<List<TimelinePostInfo>>(posts);
+ var result = _mapper.Map<List<HttpTimelinePost>>(posts);
return result;
}
@@ -217,9 +219,9 @@ namespace Timeline.Controllers try
{
- return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () =>
+ return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataETag(name, id), async () =>
{
- var data = await _service.GetPostData(name, id);
+ var data = await _postService.GetPostData(name, id);
return data;
});
}
@@ -245,7 +247,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<TimelinePostInfo>> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body)
+ public async Task<ActionResult<HttpTimelinePost>> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePostCreateRequest body)
{
var id = this.GetUserId();
if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(name, id))
@@ -255,7 +257,7 @@ namespace Timeline.Controllers var content = body.Content;
- TimelinePost post;
+ TimelinePostInfo post;
if (content.Type == TimelinePostContentTypes.Text)
{
@@ -264,7 +266,7 @@ namespace Timeline.Controllers {
return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired));
}
- post = await _service.CreateTextPost(name, id, text, body.Time);
+ post = await _postService.CreateTextPost(name, id, text, body.Time);
}
else if (content.Type == TimelinePostContentTypes.Image)
{
@@ -285,7 +287,7 @@ namespace Timeline.Controllers try
{
- post = await _service.CreateImagePost(name, id, data, body.Time);
+ post = await _postService.CreateImagePost(name, id, data, body.Time);
}
catch (ImageException)
{
@@ -297,7 +299,7 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType));
}
- var result = _mapper.Map<TimelinePostInfo>(post);
+ var result = _mapper.Map<HttpTimelinePost>(post);
return result;
}
@@ -315,13 +317,13 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<CommonDeleteResponse>> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
{
- if (!UserHasAllTimelineManagementPermission && !await _service.HasPostModifyPermission(name, id, this.GetUserId()))
+ if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(name, id, this.GetUserId()))
{
return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid());
}
try
{
- await _service.DeletePost(name, id);
+ await _postService.DeletePost(name, id);
return CommonDeleteResponse.Delete();
}
catch (TimelinePostNotExistException)
@@ -342,7 +344,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<TimelineInfo>> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body)
+ public async Task<ActionResult<HttpTimeline>> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePatchRequest body)
{
if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(name, this.GetUserId())))
{
@@ -350,7 +352,7 @@ namespace Timeline.Controllers }
await _service.ChangeProperty(name, _mapper.Map<TimelineChangePropertyRequest>(body));
var timeline = await _service.GetTimeline(name);
- var result = _mapper.Map<TimelineInfo>(timeline);
+ var result = _mapper.Map<HttpTimeline>(timeline);
return result;
}
@@ -421,14 +423,14 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public async Task<ActionResult<TimelineInfo>> TimelineCreate([FromBody] TimelineCreateRequest body)
+ public async Task<ActionResult<HttpTimeline>> TimelineCreate([FromBody] TimelineCreateRequest body)
{
var userId = this.GetUserId();
try
{
var timeline = await _service.CreateTimeline(body.Name, userId);
- var result = _mapper.Map<TimelineInfo>(timeline);
+ var result = _mapper.Map<HttpTimeline>(timeline);
return result;
}
catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline)
@@ -472,7 +474,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<TimelineInfo>> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body)
+ public async Task<ActionResult<HttpTimeline>> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body)
{
if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(body.OldName, this.GetUserId())))
{
@@ -482,7 +484,7 @@ namespace Timeline.Controllers try
{
var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName);
- return Ok(_mapper.Map<TimelineInfo>(timeline));
+ return Ok(_mapper.Map<HttpTimeline>(timeline));
}
catch (EntityAlreadyExistException)
{
diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 8f2ca600..c801b8cc 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -22,6 +22,7 @@ namespace Timeline.Controllers [ProducesErrorResponseType(typeof(CommonResponse))]
public class TokenController : Controller
{
+ private readonly IUserCredentialService _userCredentialService;
private readonly IUserTokenManager _userTokenManager;
private readonly ILogger<TokenController> _logger;
private readonly IClock _clock;
@@ -29,8 +30,9 @@ namespace Timeline.Controllers private readonly IMapper _mapper;
/// <summary></summary>
- public TokenController(IUserTokenManager userTokenManager, ILogger<TokenController> logger, IClock clock, IMapper mapper)
+ public TokenController(IUserCredentialService userCredentialService, IUserTokenManager userTokenManager, ILogger<TokenController> logger, IClock clock, IMapper mapper)
{
+ _userCredentialService = userCredentialService;
_userTokenManager = userTokenManager;
_logger = logger;
_clock = clock;
@@ -45,7 +47,7 @@ namespace Timeline.Controllers [AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult<CreateTokenResponse>> Create([FromBody] CreateTokenRequest request)
+ public async Task<ActionResult<HttpCreateTokenResponse>> Create([FromBody] HttpCreateTokenRequest request)
{
void LogFailure(string reason, Exception? e = null)
{
@@ -69,10 +71,10 @@ namespace Timeline.Controllers ("Username", request.Username),
("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default")
));
- return Ok(new CreateTokenResponse
+ return Ok(new HttpCreateTokenResponse
{
Token = result.Token,
- User = _mapper.Map<UserInfo>(result.User)
+ User = _mapper.Map<HttpUser>(result.User)
});
}
catch (UserNotExistException e)
@@ -95,7 +97,7 @@ namespace Timeline.Controllers [AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult<VerifyTokenResponse>> Verify([FromBody] VerifyTokenRequest request)
+ public async Task<ActionResult<HttpVerifyTokenResponse>> Verify([FromBody] HttpVerifyTokenRequest request)
{
void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties)
{
@@ -111,9 +113,9 @@ namespace Timeline.Controllers var result = await _userTokenManager.VerifyToken(request.Token);
_logger.LogInformation(Log.Format(LogVerifySuccess,
("Username", result.Username), ("Token", request.Token)));
- return Ok(new VerifyTokenResponse
+ return Ok(new HttpVerifyTokenResponse
{
- User = _mapper.Map<UserInfo>(result)
+ User = _mapper.Map<HttpUser>(result)
});
}
catch (UserTokenTimeExpireException e)
diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 8edae139..3727da36 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -26,21 +26,23 @@ namespace Timeline.Controllers {
private readonly ILogger<UserController> _logger;
private readonly IUserService _userService;
+ private readonly IUserCredentialService _userCredentialService;
private readonly IUserPermissionService _userPermissionService;
private readonly IUserDeleteService _userDeleteService;
private readonly IMapper _mapper;
/// <summary></summary>
- public UserController(ILogger<UserController> logger, IUserService userService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper)
+ public UserController(ILogger<UserController> logger, IUserService userService, IUserCredentialService userCredentialService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper)
{
_logger = logger;
_userService = userService;
+ _userCredentialService = userCredentialService;
_userPermissionService = userPermissionService;
_userDeleteService = userDeleteService;
_mapper = mapper;
}
- private UserInfo ConvertToUserInfo(User user) => _mapper.Map<UserInfo>(user);
+ private HttpUser ConvertToUserInfo(UserInfo user) => _mapper.Map<HttpUser>(user);
private bool UserHasUserManagementPermission => this.UserHasPermission(UserPermission.UserManagement);
@@ -50,7 +52,7 @@ namespace Timeline.Controllers /// <returns>All user list.</returns>
[HttpGet("users")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserInfo[]>> List()
+ public async Task<ActionResult<HttpUser[]>> List()
{
var users = await _userService.GetUsers();
var result = users.Select(u => ConvertToUserInfo(u)).ToArray();
@@ -65,7 +67,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserInfo>> Get([FromRoute][Username] string username)
+ public async Task<ActionResult<HttpUser>> Get([FromRoute][Username] string username)
{
try
{
@@ -92,7 +94,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserInfo>> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username)
+ public async Task<ActionResult<HttpUser>> Patch([FromBody] HttpUserPatchRequest body, [FromRoute][Username] string username)
{
if (UserHasUserManagementPermission)
{
@@ -166,7 +168,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult<UserInfo>> CreateUser([FromBody] CreateUserRequest body)
+ public async Task<ActionResult<HttpUser>> CreateUser([FromBody] HttpCreateUserRequest body)
{
try
{
@@ -186,11 +188,11 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public async Task<ActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
+ public async Task<ActionResult> ChangePassword([FromBody] HttpChangePasswordRequest request)
{
try
{
- await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword);
+ await _userCredentialService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword);
return Ok();
}
catch (BadPasswordException e)
diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs index e4203392..4205c2cf 100644 --- a/BackEnd/Timeline/Entities/DatabaseContext.cs +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -29,6 +29,8 @@ namespace Timeline.Entities public DbSet<TimelineEntity> Timelines { get; set; } = default!;
public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
+ public DbSet<HighlightTimelineEntity> HighlightTimelines { get; set; } = default!;
+
public DbSet<JwtTokenEntity> JwtToken { get; set; } = default!;
public DbSet<DataEntity> Data { get; set; } = default!;
}
diff --git a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs new file mode 100644 index 00000000..35bf6af3 --- /dev/null +++ b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs @@ -0,0 +1,31 @@ +using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Timeline.Entities
+{
+ [Table("highlight_timelines")]
+ public class HighlightTimelineEntity
+ {
+ [Key, Column("id"), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public long Id { get; set; }
+
+ [Column("timeline_id")]
+ public long TimelineId { get; set; }
+
+ [ForeignKey(nameof(TimelineId))]
+ public TimelineEntity Timeline { get; set; } = default!;
+
+ [Column("operator_id")]
+ public long? OperatorId { get; set; }
+
+ [ForeignKey(nameof(OperatorId))]
+ public UserEntity? Operator { get; set; }
+
+ [Column("add_time")]
+ public DateTime AddTime { get; set; }
+
+ [Column("order")]
+ public long Order { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs new file mode 100644 index 00000000..6cc591fa --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs @@ -0,0 +1,451 @@ +// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Entities;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20201217093401_AddHighlightTimelines")]
+ partial class AddHighlightTimelines
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.0");
+
+ modelBuilder.Entity("Timeline.Entities.DataEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<byte[]>("Data")
+ .IsRequired()
+ .HasColumnType("BLOB")
+ .HasColumnName("data");
+
+ b.Property<int>("Ref")
+ .HasColumnType("INTEGER")
+ .HasColumnName("ref");
+
+ b.Property<string>("Tag")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("tag");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Tag")
+ .IsUnique();
+
+ b.ToTable("data");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("AddTime")
+ .HasColumnType("TEXT")
+ .HasColumnName("add_time");
+
+ b.Property<long?>("OperatorId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("operator_id");
+
+ b.Property<long>("Order")
+ .HasColumnType("INTEGER")
+ .HasColumnName("order");
+
+ b.Property<long>("TimelineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("timeline_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OperatorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("highlight_timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<byte[]>("Key")
+ .IsRequired()
+ .HasColumnType("BLOB")
+ .HasColumnName("key");
+
+ b.HasKey("Id");
+
+ b.ToTable("jwt_token");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreateTime")
+ .HasColumnType("TEXT")
+ .HasColumnName("create_time");
+
+ b.Property<long>("CurrentPostLocalId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("current_post_local_id");
+
+ b.Property<string>("Description")
+ .HasColumnType("TEXT")
+ .HasColumnName("description");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_modified");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT")
+ .HasColumnName("name");
+
+ b.Property<DateTime>("NameLastModified")
+ .HasColumnType("TEXT")
+ .HasColumnName("name_last_modified");
+
+ b.Property<long>("OwnerId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("owner");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT")
+ .HasColumnName("title");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("unique_id")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<int>("Visibility")
+ .HasColumnType("INTEGER")
+ .HasColumnName("visibility");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("timelines");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<long>("TimelineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("timeline");
+
+ b.Property<long>("UserId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("user");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TimelineId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("timeline_members");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<long?>("AuthorId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("author");
+
+ b.Property<string>("Content")
+ .HasColumnType("TEXT")
+ .HasColumnName("content");
+
+ b.Property<string>("ContentType")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("content_type");
+
+ b.Property<string>("ExtraContent")
+ .HasColumnType("TEXT")
+ .HasColumnName("extra_content");
+
+ b.Property<DateTime>("LastUpdated")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_updated");
+
+ b.Property<long>("LocalId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("local_id");
+
+ b.Property<DateTime>("Time")
+ .HasColumnType("TEXT")
+ .HasColumnName("time");
+
+ b.Property<long>("TimelineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("timeline");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("timeline_posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<string>("DataTag")
+ .HasColumnType("TEXT")
+ .HasColumnName("data_tag");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT")
+ .HasColumnName("last_modified");
+
+ b.Property<string>("Type")
+ .HasColumnType("TEXT")
+ .HasColumnName("type");
+
+ b.Property<long>("UserId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("user");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_avatars");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("CreateTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("create_time")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<DateTime>("LastModified")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("last_modified")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<string>("Nickname")
+ .HasColumnType("TEXT")
+ .HasColumnName("nickname");
+
+ b.Property<string>("Password")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("password");
+
+ b.Property<string>("UniqueId")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("unique_id")
+ .HasDefaultValueSql("lower(hex(randomblob(16)))");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("username");
+
+ b.Property<DateTime>("UsernameChangeTime")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("username_change_time")
+ .HasDefaultValueSql("datetime('now', 'utc')");
+
+ b.Property<long>("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0L)
+ .HasColumnName("version");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("users");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<string>("Permission")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("permission");
+
+ b.Property<long>("UserId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("user_permission");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Operator")
+ .WithMany()
+ .HasForeignKey("OperatorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany()
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Operator");
+
+ b.Navigation("Timeline");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Owner")
+ .WithMany("Timelines")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Members")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("TimelinesJoined")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Timeline");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Author")
+ .WithMany("TimelinePosts")
+ .HasForeignKey("AuthorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany("Posts")
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Author");
+
+ b.Navigation("Timeline");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithOne("Avatar")
+ .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "User")
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
+ {
+ b.Navigation("Members");
+
+ b.Navigation("Posts");
+ });
+
+ modelBuilder.Entity("Timeline.Entities.UserEntity", b =>
+ {
+ b.Navigation("Avatar");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("TimelinePosts");
+
+ b.Navigation("Timelines");
+
+ b.Navigation("TimelinesJoined");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs new file mode 100644 index 00000000..e838615e --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs @@ -0,0 +1,55 @@ +using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddHighlightTimelines : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "highlight_timelines",
+ columns: table => new
+ {
+ id = table.Column<long>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ timeline_id = table.Column<long>(type: "INTEGER", nullable: false),
+ operator_id = table.Column<long>(type: "INTEGER", nullable: true),
+ add_time = table.Column<DateTime>(type: "TEXT", nullable: false),
+ order = table.Column<long>(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_highlight_timelines", x => x.id);
+ table.ForeignKey(
+ name: "FK_highlight_timelines_timelines_timeline_id",
+ column: x => x.timeline_id,
+ principalTable: "timelines",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_highlight_timelines_users_operator_id",
+ column: x => x.operator_id,
+ principalTable: "users",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_highlight_timelines_operator_id",
+ table: "highlight_timelines",
+ column: "operator_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_highlight_timelines_timeline_id",
+ table: "highlight_timelines",
+ column: "timeline_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "highlight_timelines");
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 2f0f75a2..ea3378dc 100644 --- a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -45,6 +45,38 @@ namespace Timeline.Migrations b.ToTable("data");
});
+ modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property<DateTime>("AddTime")
+ .HasColumnType("TEXT")
+ .HasColumnName("add_time");
+
+ b.Property<long?>("OperatorId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("operator_id");
+
+ b.Property<long>("Order")
+ .HasColumnType("INTEGER")
+ .HasColumnName("order");
+
+ b.Property<long>("TimelineId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("timeline_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OperatorId");
+
+ b.HasIndex("TimelineId");
+
+ b.ToTable("highlight_timelines");
+ });
+
modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b =>
{
b.Property<long>("Id")
@@ -306,6 +338,23 @@ namespace Timeline.Migrations b.ToTable("user_permission");
});
+ modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b =>
+ {
+ b.HasOne("Timeline.Entities.UserEntity", "Operator")
+ .WithMany()
+ .HasForeignKey("OperatorId");
+
+ b.HasOne("Timeline.Entities.TimelineEntity", "Timeline")
+ .WithMany()
+ .HasForeignKey("TimelineId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Operator");
+
+ b.Navigation("Timeline");
+ });
+
modelBuilder.Entity("Timeline.Entities.TimelineEntity", b =>
{
b.HasOne("Timeline.Entities.UserEntity", "Owner")
diff --git a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs new file mode 100644 index 00000000..e5aed068 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations;
+using Timeline.Models.Validation;
+
+namespace Timeline.Models.Http
+{
+ /// <summary>
+ /// Move highlight timeline request body model.
+ /// </summary>
+ public class HttpHighlightTimelineMoveRequest
+ {
+ [GeneralTimelineName]
+ public string Timeline { get; set; } = default!;
+
+ [Required]
+ public long? NewPosition { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Models/Http/Timeline.cs b/BackEnd/Timeline/Models/Http/Timeline.cs index a81b33f5..8e3831e1 100644 --- a/BackEnd/Timeline/Models/Http/Timeline.cs +++ b/BackEnd/Timeline/Models/Http/Timeline.cs @@ -11,7 +11,7 @@ namespace Timeline.Models.Http /// <summary>
/// Info of post content.
/// </summary>
- public class TimelinePostContentInfo
+ public class HttpTimelinePostContent
{
/// <summary>
/// Type of the post content.
@@ -34,7 +34,7 @@ namespace Timeline.Models.Http /// <summary>
/// Info of a post.
/// </summary>
- public class TimelinePostInfo
+ public class HttpTimelinePost
{
/// <summary>
/// Post id.
@@ -43,7 +43,7 @@ namespace Timeline.Models.Http /// <summary>
/// Content of the post. May be null if post is deleted.
/// </summary>
- public TimelinePostContentInfo? Content { get; set; }
+ public HttpTimelinePostContent? Content { get; set; }
/// <summary>
/// True if post is deleted.
/// </summary>
@@ -55,7 +55,7 @@ namespace Timeline.Models.Http /// <summary>
/// The author. May be null if the user has been deleted.
/// </summary>
- public UserInfo? Author { get; set; } = default!;
+ public HttpUser? Author { get; set; } = default!;
/// <summary>
/// Last updated time.
/// </summary>
@@ -65,7 +65,7 @@ namespace Timeline.Models.Http /// <summary>
/// Info of a timeline.
/// </summary>
- public class TimelineInfo
+ public class HttpTimeline
{
/// <summary>
/// Unique id.
@@ -90,7 +90,7 @@ namespace Timeline.Models.Http /// <summary>
/// Owner of the timeline.
/// </summary>
- public UserInfo Owner { get; set; } = default!;
+ public HttpUser Owner { get; set; } = default!;
/// <summary>
/// Visibility of the timeline.
/// </summary>
@@ -99,7 +99,7 @@ namespace Timeline.Models.Http /// <summary>
/// Members of timeline.
/// </summary>
- public List<UserInfo> Members { get; set; } = default!;
+ public List<HttpUser> Members { get; set; } = default!;
#pragma warning restore CA2227 // Collection properties should be read only
/// <summary>
/// Create time of timeline.
@@ -114,14 +114,14 @@ namespace Timeline.Models.Http /// <summary>
/// Related links.
/// </summary>
- public TimelineInfoLinks _links { get; set; } = default!;
+ public HttpTimelineLinks _links { get; set; } = default!;
#pragma warning restore CA1707 // Identifiers should not contain underscores
}
/// <summary>
/// Related links for timeline.
/// </summary>
- public class TimelineInfoLinks
+ public class HttpTimelineLinks
{
/// <summary>
/// Self.
@@ -133,23 +133,23 @@ namespace Timeline.Models.Http public string Posts { get; set; } = default!;
}
- public class TimelineInfoLinksValueResolver : IValueResolver<Timeline, TimelineInfo, TimelineInfoLinks>
+ public class HttpTimelineLinksValueResolver : IValueResolver<TimelineInfo, HttpTimeline, HttpTimelineLinks>
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IUrlHelperFactory _urlHelperFactory;
- public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
+ public HttpTimelineLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
{
_actionContextAccessor = actionContextAccessor;
_urlHelperFactory = urlHelperFactory;
}
- public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context)
+ public HttpTimelineLinks Resolve(TimelineInfo source, HttpTimeline destination, HttpTimelineLinks destMember, ResolutionContext context)
{
var actionContext = _actionContextAccessor.AssertActionContextForUrlFill();
var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext);
- return new TimelineInfoLinks
+ return new HttpTimelineLinks
{
Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }),
Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name })
@@ -157,18 +157,18 @@ namespace Timeline.Models.Http }
}
- public class TimelinePostContentResolver : IValueResolver<TimelinePost, TimelinePostInfo, TimelinePostContentInfo?>
+ public class HttpTimelinePostContentResolver : IValueResolver<TimelinePostInfo, HttpTimelinePost, HttpTimelinePostContent?>
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IUrlHelperFactory _urlHelperFactory;
- public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
+ public HttpTimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
{
_actionContextAccessor = actionContextAccessor;
_urlHelperFactory = urlHelperFactory;
}
- public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context)
+ public HttpTimelinePostContent? Resolve(TimelinePostInfo source, HttpTimelinePost destination, HttpTimelinePostContent? destMember, ResolutionContext context)
{
var actionContext = _actionContextAccessor.AssertActionContextForUrlFill();
var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext);
@@ -182,7 +182,7 @@ namespace Timeline.Models.Http if (sourceContent is TextTimelinePostContent textContent)
{
- return new TimelinePostContentInfo
+ return new HttpTimelinePostContent
{
Type = TimelinePostContentTypes.Text,
Text = textContent.Text
@@ -190,7 +190,7 @@ namespace Timeline.Models.Http }
else if (sourceContent is ImageTimelinePostContent imageContent)
{
- return new TimelinePostContentInfo
+ return new HttpTimelinePostContent
{
Type = TimelinePostContentTypes.Image,
Url = urlHelper.ActionLink(
@@ -207,13 +207,12 @@ namespace Timeline.Models.Http }
}
- public class TimelineInfoAutoMapperProfile : Profile
+ public class HttpTimelineAutoMapperProfile : Profile
{
- public TimelineInfoAutoMapperProfile()
+ public HttpTimelineAutoMapperProfile()
{
- CreateMap<Timeline, TimelineInfo>().ForMember(u => u._links, opt => opt.MapFrom<TimelineInfoLinksValueResolver>());
- CreateMap<TimelinePost, TimelinePostInfo>().ForMember(p => p.Content, opt => opt.MapFrom<TimelinePostContentResolver>());
- CreateMap<TimelinePatchRequest, TimelineChangePropertyRequest>();
+ CreateMap<TimelineInfo, HttpTimeline>().ForMember(u => u._links, opt => opt.MapFrom<HttpTimelineLinksValueResolver>());
+ CreateMap<TimelinePostInfo, HttpTimelinePost>().ForMember(p => p.Content, opt => opt.MapFrom<HttpTimelinePostContentResolver>());
}
}
}
diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs index 7bd141ed..42a926fd 100644 --- a/BackEnd/Timeline/Models/Http/TimelineController.cs +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -1,4 +1,5 @@ -using System;
+using AutoMapper;
+using System;
using System.ComponentModel.DataAnnotations;
using Timeline.Models.Validation;
@@ -7,7 +8,7 @@ namespace Timeline.Models.Http /// <summary>
/// Content of post create request.
/// </summary>
- public class TimelinePostCreateRequestContent
+ public class HttpTimelinePostCreateRequestContent
{
/// <summary>
/// Type of post content.
@@ -24,13 +25,13 @@ namespace Timeline.Models.Http public string? Data { get; set; }
}
- public class TimelinePostCreateRequest
+ public class HttpTimelinePostCreateRequest
{
/// <summary>
/// Content of the new post.
/// </summary>
[Required]
- public TimelinePostCreateRequestContent Content { get; set; } = default!;
+ public HttpTimelinePostCreateRequestContent Content { get; set; } = default!;
/// <summary>
/// Time of the post. If not set, current time will be used.
@@ -54,7 +55,7 @@ namespace Timeline.Models.Http /// <summary>
/// Patch timeline request model.
/// </summary>
- public class TimelinePatchRequest
+ public class HttpTimelinePatchRequest
{
/// <summary>
/// New title. Null for not change.
@@ -75,7 +76,7 @@ namespace Timeline.Models.Http /// <summary>
/// Change timeline name request model.
/// </summary>
- public class TimelineChangeNameRequest
+ public class HttpTimelineChangeNameRequest
{
/// <summary>
/// Old name of timeline.
@@ -90,4 +91,12 @@ namespace Timeline.Models.Http [TimelineName]
public string NewName { get; set; } = default!;
}
+
+ public class HttpTimelineControllerAutoMapperProfile : Profile
+ {
+ public HttpTimelineControllerAutoMapperProfile()
+ {
+ CreateMap<HttpTimelinePatchRequest, TimelineChangePropertyRequest>();
+ }
+ }
}
diff --git a/BackEnd/Timeline/Models/Http/TokenController.cs b/BackEnd/Timeline/Models/Http/TokenController.cs index a42c44e5..a5cbba14 100644 --- a/BackEnd/Timeline/Models/Http/TokenController.cs +++ b/BackEnd/Timeline/Models/Http/TokenController.cs @@ -4,9 +4,9 @@ using Timeline.Controllers; namespace Timeline.Models.Http
{
/// <summary>
- /// Request model for <see cref="TokenController.Create(CreateTokenRequest)"/>.
+ /// Request model for <see cref="TokenController.Create(HttpCreateTokenRequest)"/>.
/// </summary>
- public class CreateTokenRequest
+ public class HttpCreateTokenRequest
{
/// <summary>
/// The username.
@@ -24,9 +24,9 @@ namespace Timeline.Models.Http }
/// <summary>
- /// Response model for <see cref="TokenController.Create(CreateTokenRequest)"/>.
+ /// Response model for <see cref="TokenController.Create(HttpCreateTokenRequest)"/>.
/// </summary>
- public class CreateTokenResponse
+ public class HttpCreateTokenResponse
{
/// <summary>
/// The token created.
@@ -35,13 +35,13 @@ namespace Timeline.Models.Http /// <summary>
/// The user owning the token.
/// </summary>
- public UserInfo User { get; set; } = default!;
+ public HttpUser User { get; set; } = default!;
}
/// <summary>
- /// Request model for <see cref="TokenController.Verify(VerifyTokenRequest)"/>.
+ /// Request model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
/// </summary>
- public class VerifyTokenRequest
+ public class HttpVerifyTokenRequest
{
/// <summary>
/// The token to verify.
@@ -50,13 +50,13 @@ namespace Timeline.Models.Http }
/// <summary>
- /// Response model for <see cref="TokenController.Verify(VerifyTokenRequest)"/>.
+ /// Response model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
/// </summary>
- public class VerifyTokenResponse
+ public class HttpVerifyTokenResponse
{
/// <summary>
/// The user owning the token.
/// </summary>
- public UserInfo User { get; set; } = default!;
+ public HttpUser User { get; set; } = default!;
}
}
diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/User.cs index 0f865172..bdb40b9f 100644 --- a/BackEnd/Timeline/Models/Http/UserInfo.cs +++ b/BackEnd/Timeline/Models/Http/User.cs @@ -11,7 +11,7 @@ namespace Timeline.Models.Http /// <summary>
/// Info of a user.
/// </summary>
- public class UserInfo
+ public class HttpUser
{
/// <summary>
/// Unique id.
@@ -35,14 +35,14 @@ namespace Timeline.Models.Http /// <summary>
/// Related links.
/// </summary>
- public UserInfoLinks _links { get; set; } = default!;
+ public HttpUserLinks _links { get; set; } = default!;
#pragma warning restore CA1707 // Identifiers should not contain underscores
}
/// <summary>
/// Related links for user.
/// </summary>
- public class UserInfoLinks
+ public class HttpUserLinks
{
/// <summary>
/// Self.
@@ -58,7 +58,7 @@ namespace Timeline.Models.Http public string Timeline { get; set; } = default!;
}
- public class UserPermissionsValueConverter : ITypeConverter<UserPermissions, List<string>>
+ public class HttpUserPermissionsValueConverter : ITypeConverter<UserPermissions, List<string>>
{
public List<string> Convert(UserPermissions source, List<string> destination, ResolutionContext context)
{
@@ -66,23 +66,23 @@ namespace Timeline.Models.Http }
}
- public class UserInfoLinksValueResolver : IValueResolver<User, UserInfo, UserInfoLinks>
+ public class HttpUserLinksValueResolver : IValueResolver<UserInfo, HttpUser, HttpUserLinks>
{
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IUrlHelperFactory _urlHelperFactory;
- public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
+ public HttpUserLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
{
_actionContextAccessor = actionContextAccessor;
_urlHelperFactory = urlHelperFactory;
}
- public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context)
+ public HttpUserLinks Resolve(UserInfo source, HttpUser destination, HttpUserLinks destMember, ResolutionContext context)
{
var actionContext = _actionContextAccessor.AssertActionContextForUrlFill();
var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext);
- var result = new UserInfoLinks
+ var result = new HttpUserLinks
{
Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }),
Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }),
@@ -92,14 +92,14 @@ namespace Timeline.Models.Http }
}
- public class UserInfoAutoMapperProfile : Profile
+ public class HttpUserAutoMapperProfile : Profile
{
- public UserInfoAutoMapperProfile()
+ public HttpUserAutoMapperProfile()
{
CreateMap<UserPermissions, List<string>>()
- .ConvertUsing<UserPermissionsValueConverter>();
- CreateMap<User, UserInfo>()
- .ForMember(u => u._links, opt => opt.MapFrom<UserInfoLinksValueResolver>());
+ .ConvertUsing<HttpUserPermissionsValueConverter>();
+ CreateMap<UserInfo, HttpUser>()
+ .ForMember(u => u._links, opt => opt.MapFrom<HttpUserLinksValueResolver>());
}
}
}
diff --git a/BackEnd/Timeline/Models/Http/UserController.cs b/BackEnd/Timeline/Models/Http/UserController.cs index 92a63874..1b4d09ec 100644 --- a/BackEnd/Timeline/Models/Http/UserController.cs +++ b/BackEnd/Timeline/Models/Http/UserController.cs @@ -7,9 +7,9 @@ using Timeline.Services; namespace Timeline.Models.Http
{
/// <summary>
- /// Request model for <see cref="UserController.Patch(UserPatchRequest, string)"/>.
+ /// Request model for <see cref="UserController.Patch(HttpUserPatchRequest, string)"/>.
/// </summary>
- public class UserPatchRequest
+ public class HttpUserPatchRequest
{
/// <summary>
/// New username. Null if not change. Need to be administrator.
@@ -31,9 +31,9 @@ namespace Timeline.Models.Http }
/// <summary>
- /// Request model for <see cref="UserController.CreateUser(CreateUserRequest)"/>.
+ /// Request model for <see cref="UserController.CreateUser(HttpCreateUserRequest)"/>.
/// </summary>
- public class CreateUserRequest
+ public class HttpCreateUserRequest
{
/// <summary>
/// Username of the new user.
@@ -49,9 +49,9 @@ namespace Timeline.Models.Http }
/// <summary>
- /// Request model for <see cref="UserController.ChangePassword(ChangePasswordRequest)"/>.
+ /// Request model for <see cref="UserController.ChangePassword(HttpChangePasswordRequest)"/>.
/// </summary>
- public class ChangePasswordRequest
+ public class HttpChangePasswordRequest
{
/// <summary>
/// Old password.
@@ -66,11 +66,11 @@ namespace Timeline.Models.Http public string NewPassword { get; set; } = default!;
}
- public class UserControllerAutoMapperProfile : Profile
+ public class HttpUserControllerModelAutoMapperProfile : Profile
{
- public UserControllerAutoMapperProfile()
+ public HttpUserControllerModelAutoMapperProfile()
{
- CreateMap<UserPatchRequest, ModifyUserParams>();
+ CreateMap<HttpUserPatchRequest, ModifyUserParams>();
}
}
}
diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/TimelineInfo.cs index a5987577..649af274 100644 --- a/BackEnd/Timeline/Models/Timeline.cs +++ b/BackEnd/Timeline/Models/TimelineInfo.cs @@ -50,9 +50,14 @@ namespace Timeline.Models public string DataTag { get; set; }
}
- public class TimelinePost
+ public class TimelinePostInfo
{
- public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName)
+ public TimelinePostInfo()
+ {
+
+ }
+
+ public TimelinePostInfo(long id, ITimelinePostContent? content, DateTime time, UserInfo? author, DateTime lastUpdated, string timelineName)
{
Id = id;
Content = content;
@@ -66,24 +71,51 @@ namespace Timeline.Models public ITimelinePostContent? Content { get; set; }
public bool Deleted => Content == null;
public DateTime Time { get; set; }
- public User? Author { get; set; }
+ public UserInfo? Author { get; set; }
public DateTime LastUpdated { get; set; }
- public string TimelineName { get; set; }
+ public string TimelineName { get; set; } = default!;
}
-#pragma warning disable CA1724 // Type names should not match namespaces
- public class Timeline
-#pragma warning restore CA1724 // Type names should not match namespaces
+ public class TimelineInfo
{
- public string UniqueID { get; set; } = default!;
+ public TimelineInfo()
+ {
+
+ }
+
+ public TimelineInfo(
+ string uniqueId,
+ string name,
+ DateTime nameLastModified,
+ string title,
+ string description,
+ UserInfo owner,
+ TimelineVisibility visibility,
+ List<UserInfo> members,
+ DateTime createTime,
+ DateTime lastModified)
+ {
+ UniqueId = uniqueId;
+ Name = name;
+ NameLastModified = nameLastModified;
+ Title = title;
+ Description = description;
+ Owner = owner;
+ Visibility = visibility;
+ Members = members;
+ CreateTime = createTime;
+ LastModified = lastModified;
+ }
+
+ public string UniqueId { get; set; } = default!;
public string Name { get; set; } = default!;
public DateTime NameLastModified { get; set; } = default!;
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
- public User Owner { get; set; } = default!;
+ public UserInfo Owner { get; set; } = default!;
public TimelineVisibility Visibility { get; set; }
#pragma warning disable CA2227 // Collection properties should be read only
- public List<User> Members { get; set; } = default!;
+ public List<UserInfo> Members { get; set; } = default!;
#pragma warning restore CA2227 // Collection properties should be read only
public DateTime CreateTime { get; set; } = default!;
public DateTime LastModified { get; set; } = default!;
diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs deleted file mode 100644 index ae2afe85..00000000 --- a/BackEnd/Timeline/Models/User.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System;
-using Timeline.Services;
-
-namespace Timeline.Models
-{
- public record User
- {
- public long Id { get; set; }
- public string UniqueId { get; set; } = default!;
-
- public string Username { get; set; } = default!;
- public string Nickname { get; set; } = default!;
-
- public UserPermissions Permissions { get; set; } = default!;
-
- public DateTime UsernameChangeTime { get; set; }
- public DateTime CreateTime { get; set; }
- public DateTime LastModified { get; set; }
- public long Version { get; set; }
- }
-}
diff --git a/BackEnd/Timeline/Models/UserInfo.cs b/BackEnd/Timeline/Models/UserInfo.cs new file mode 100644 index 00000000..e8d57def --- /dev/null +++ b/BackEnd/Timeline/Models/UserInfo.cs @@ -0,0 +1,48 @@ +using System;
+using Timeline.Services;
+
+namespace Timeline.Models
+{
+ public class UserInfo
+ {
+ public UserInfo()
+ {
+
+ }
+
+ public UserInfo(
+ long id,
+ string uniqueId,
+ string username,
+ string nickname,
+ UserPermissions permissions,
+ DateTime usernameChangeTime,
+ DateTime createTime,
+ DateTime lastModified,
+ long version)
+ {
+ Id = id;
+ UniqueId = uniqueId;
+ Username = username;
+ Nickname = nickname;
+ Permissions = permissions;
+ UsernameChangeTime = usernameChangeTime;
+ CreateTime = createTime;
+ LastModified = lastModified;
+ Version = version;
+ }
+
+ public long Id { get; set; }
+ public string UniqueId { get; set; } = default!;
+
+ public string Username { get; set; } = default!;
+ public string Nickname { get; set; } = default!;
+
+ public UserPermissions Permissions { get; set; } = default!;
+
+ public DateTime UsernameChangeTime { get; set; }
+ public DateTime CreateTime { get; set; }
+ public DateTime LastModified { get; set; }
+ public long Version { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/BasicTimelineService.cs b/BackEnd/Timeline/Services/BasicTimelineService.cs new file mode 100644 index 00000000..0d9f64a9 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicTimelineService.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic timeline functions, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicTimelineService
+ {
+ /// <summary>
+ /// Get the timeline id by name.
+ /// </summary>
+ /// <param name="timelineName">Timeline name.</param>
+ /// <returns>Id of the timeline.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <remarks>
+ /// If name is of personal timeline and the timeline does not exist, it will be created if user exists.
+ /// If the user does not exist, <see cref="TimelineNotExistException"/> will be thrown with <see cref="UserNotExistException"/> as inner exception.
+ ///</remarks>
+ Task<long> GetTimelineIdByName(string timelineName);
+ }
+
+
+ public class BasicTimelineService : IBasicTimelineService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly IBasicUserService _basicUserService;
+ private readonly IClock _clock;
+
+ private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator();
+
+ public BasicTimelineService(DatabaseContext database, IBasicUserService basicUserService, IClock clock)
+ {
+ _database = database;
+ _basicUserService = basicUserService;
+ _clock = clock;
+ }
+
+ protected TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
+ {
+ var currentTime = _clock.GetCurrentTime();
+
+ return new TimelineEntity
+ {
+ Name = name,
+ NameLastModified = currentTime,
+ OwnerId = ownerId,
+ Visibility = TimelineVisibility.Register,
+ CreateTime = currentTime,
+ LastModified = currentTime,
+ CurrentPostLocalId = 0,
+ Members = new List<TimelineMemberEntity>()
+ };
+ }
+
+ public async Task<long> GetTimelineIdByName(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ if (!_generalTimelineNameValidator.Validate(timelineName, out var message))
+ throw new ArgumentException(message);
+
+ timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+
+ if (isPersonal)
+ {
+ long userId;
+ try
+ {
+ userId = await _basicUserService.GetUserIdByUsername(timelineName);
+ }
+ catch (UserNotExistException e)
+ {
+ throw new TimelineNotExistException(timelineName, e);
+ }
+
+ var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity != null)
+ {
+ return timelineEntity.Id;
+ }
+ else
+ {
+ var newTimelineEntity = CreateNewTimelineEntity(null, userId);
+ _database.Timelines.Add(newTimelineEntity);
+ await _database.SaveChangesAsync();
+
+ return newTimelineEntity.Id;
+ }
+ }
+ else
+ {
+ var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync();
+
+ if (timelineEntity == null)
+ {
+ throw new TimelineNotExistException(timelineName);
+ }
+ else
+ {
+ return timelineEntity.Id;
+ }
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs new file mode 100644 index 00000000..fbbb6677 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// This service provide some basic user features, which should be used internally for other services.
+ /// </summary>
+ public interface IBasicUserService
+ {
+ /// <summary>
+ /// Check if a user exists.
+ /// </summary>
+ /// <param name="id">The id of the user.</param>
+ /// <returns>True if exists. Otherwise false.</returns>
+ Task<bool> CheckUserExistence(long id);
+
+ /// <summary>
+ /// Get the user id of given username.
+ /// </summary>
+ /// <param name="username">Username of the user.</param>
+ /// <returns>The id of the user.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsername(string username);
+ }
+
+ public class BasicUserService : IBasicUserService
+ {
+ private readonly DatabaseContext _database;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public BasicUserService(DatabaseContext database)
+ {
+ _database = database;
+ }
+
+ public async Task<bool> CheckUserExistence(long id)
+ {
+ return await _database.Users.AnyAsync(u => u.Id == id);
+ }
+
+ public async Task<long> GetUserIdByUsername(string username)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+
+ if (!_usernameValidator.Validate(username, out var message))
+ throw new ArgumentException(message);
+
+ var entity = await _database.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ return entity.Id;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs index 70970b24..ef882ffe 100644 --- a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs +++ b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs @@ -6,7 +6,11 @@ namespace Timeline.Services.Exceptions [Serializable]
public class TimelineNotExistException : EntityNotExistException
{
- public TimelineNotExistException() : this(null, null) { }
+ public TimelineNotExistException() : this((long?)null) { }
+ public TimelineNotExistException(long? id) : this(id, null) { }
+ public TimelineNotExistException(long? id, Exception? inner) : this(id, null, inner) { }
+ public TimelineNotExistException(long? id, string? message, Exception? inner) : base(EntityNames.Timeline, null, message, inner) { TimelineId = id; }
+
public TimelineNotExistException(string? timelineName) : this(timelineName, null) { }
public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { }
public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null)
@@ -17,5 +21,6 @@ namespace Timeline.Services.Exceptions System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public string? TimelineName { get; set; }
+ public long? TimelineId { get; set; }
}
}
diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs new file mode 100644 index 00000000..b19efe21 --- /dev/null +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -0,0 +1,194 @@ +using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class InvalidHighlightTimelineException : Exception
+ {
+ public InvalidHighlightTimelineException() { }
+ public InvalidHighlightTimelineException(string message) : base(message) { }
+ public InvalidHighlightTimelineException(string message, Exception inner) : base(message, inner) { }
+ protected InvalidHighlightTimelineException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+
+ /// <summary>
+ /// Service that controls highlight timeline.
+ /// </summary>
+ public interface IHighlightTimelineService
+ {
+ /// <summary>
+ /// Get all highlight timelines in order.
+ /// </summary>
+ /// <returns>A list of all highlight timelines.</returns>
+ Task<List<TimelineInfo>> GetHighlightTimelines();
+
+ /// <summary>
+ /// Add a timeline to highlight list.
+ /// </summary>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given name does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task AddHighlightTimeline(string timelineName, long? operatorId);
+
+ /// <summary>
+ /// Remove a timeline from highlight list.
+ /// </summary>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <param name="operatorId">The user id of operator.</param>
+ /// <returns>True if deletion is actually performed. Otherwise false (timeline was not in the list).</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given name does not exist.</exception>
+ /// <exception cref="UserNotExistException">Thrown when user with given operator id does not exist.</exception>
+ Task<bool> RemoveHighlightTimeline(string timelineName, long? operatorId);
+
+ /// <summary>
+ /// Move a highlight timeline to a new position.
+ /// </summary>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <param name="newPosition">The new position. Starts at 1.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given name does not exist.</exception>
+ /// <exception cref="InvalidHighlightTimelineException">Thrown when given timeline is not a highlight timeline.</exception>
+ /// <remarks>
+ /// If <paramref name="newPosition"/> is smaller than 1. Then move the timeline to head.
+ /// If <paramref name="newPosition"/> is bigger than total count. Then move the timeline to tail.
+ /// </remarks>
+ Task MoveHighlightTimeline(string timelineName, long newPosition);
+ }
+
+ public class HighlightTimelineService : IHighlightTimelineService
+ {
+ private readonly DatabaseContext _database;
+ private readonly IBasicUserService _userService;
+ private readonly ITimelineService _timelineService;
+ private readonly IClock _clock;
+
+ public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService, IClock clock)
+ {
+ _database = database;
+ _userService = userService;
+ _timelineService = timelineService;
+ _clock = clock;
+ }
+
+ public async Task AddHighlightTimeline(string timelineName, long? operatorId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timelineName);
+
+ if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value))
+ {
+ throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null);
+ }
+
+ var alreadyIs = await _database.HighlightTimelines.AnyAsync(t => t.TimelineId == timelineId);
+
+ if (alreadyIs) return;
+
+ _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime(), Order = await _database.HighlightTimelines.CountAsync() + 1 });
+ await _database.SaveChangesAsync();
+ }
+
+ public async Task<List<TimelineInfo>> GetHighlightTimelines()
+ {
+ var entities = await _database.HighlightTimelines.OrderBy(t => t.Order).Select(t => new { t.TimelineId }).ToListAsync();
+
+ var result = new List<TimelineInfo>();
+
+ foreach (var entity in entities)
+ {
+ result.Add(await _timelineService.GetTimelineById(entity.TimelineId));
+ }
+
+ return result;
+ }
+
+ public async Task<bool> RemoveHighlightTimeline(string timelineName, long? operatorId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timelineName);
+
+ if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value))
+ {
+ throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null);
+ }
+
+ var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId);
+
+ if (entity == null) return false;
+
+ await using var transaction = await _database.Database.BeginTransactionAsync();
+
+ var order = entity.Order;
+
+ _database.HighlightTimelines.Remove(entity);
+ await _database.SaveChangesAsync();
+
+ await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` > {0}", order);
+
+ await transaction.CommitAsync();
+
+ return true;
+ }
+
+ public async Task MoveHighlightTimeline(string timelineName, long newPosition)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _timelineService.GetTimelineIdByName(timelineName);
+
+ var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId);
+
+ if (entity == null) throw new InvalidHighlightTimelineException("You can't move a non-highlight timeline.");
+
+ var oldPosition = entity.Order;
+
+ if (newPosition < 1)
+ {
+ newPosition = 1;
+ }
+ else
+ {
+ var totalCount = await _database.HighlightTimelines.CountAsync();
+ if (newPosition > totalCount) newPosition = totalCount;
+ }
+
+ if (oldPosition == newPosition) return;
+
+ await using var transaction = await _database.Database.BeginTransactionAsync();
+
+ if (newPosition > oldPosition)
+ {
+ await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` BETWEEN {0} AND {1}", oldPosition + 1, newPosition);
+ await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id);
+ }
+ else
+ {
+ await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` + 1 WHERE `order` BETWEEN {0} AND {1}", newPosition, oldPosition - 1);
+ await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id);
+ }
+
+ await transaction.CommitAsync();
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..35513a36 --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SixLabors.ImageSharp;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models;
+using Timeline.Services.Exceptions;
+using static Timeline.Resources.Services.TimelineService;
+
+namespace Timeline.Services
+{
+ public class PostData : ICacheableData
+ {
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; } = default!;
+#pragma warning restore CA1819 // Properties should not return arrays
+ public string Type { get; set; } = default!;
+ public string ETag { get; set; } = default!;
+ public DateTime? LastModified { get; set; } // TODO: Why nullable?
+ }
+
+ public interface ITimelinePostService
+ {
+ /// <summary>
+ /// Get all the posts in the timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="modifiedSince">The time that posts have been modified since.</param>
+ /// <param name="includeDeleted">Whether include deleted posts.</param>
+ /// <returns>A list of all posts.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ Task<List<TimelinePostInfo>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
+
+ /// <summary>
+ /// Get the etag of data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostData(string, long)"/>
+ Task<string> GetPostDataETag(string timelineName, long postId);
+
+ /// <summary>
+ /// Get the data of a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline of the post.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
+ /// <seealso cref="GetPostDataETag(string, long)"/>
+ Task<PostData> GetPostData(string timelineName, long postId);
+
+ /// <summary>
+ /// Create a new text post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="text">The content text.</param>
+ /// <param name="time">The time of the post. If null, then current time is used.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ Task<TimelinePostInfo> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
+
+ /// <summary>
+ /// Create a new image post in timeline.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to create post against.</param>
+ /// <param name="authorId">The author's user id.</param>
+ /// <param name="imageData">The image data.</param>
+ /// <param name="time">The time of the post. If null, then use current time.</param>
+ /// <returns>The info of the created post.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="imageData"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
+ /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
+ Task<TimelinePostInfo> CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time);
+
+ /// <summary>
+ /// Delete a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline to delete post against.</param>
+ /// <param name="postId">The id of the post to delete.</param>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
+ /// <remarks>
+ /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
+ /// </remarks>
+ Task DeletePost(string timelineName, long postId);
+
+ /// <summary>
+ /// Delete all posts of the given user. Used when delete a user.
+ /// </summary>
+ /// <param name="userId">The id of the user.</param>
+ Task DeleteAllPostsOfUser(long userId);
+
+ /// <summary>
+ /// Verify whether a user has the permission to modify a post.
+ /// </summary>
+ /// <param name="timelineName">The name of the timeline.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <param name="modifierId">The id of the user to check on.</param>
+ /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
+ /// <returns>True if can modify, false if can't modify.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
+ /// <exception cref="TimelineNotExistException">
+ /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
+ /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
+ /// </exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
+ /// <remarks>
+ /// Unless <paramref name="throwOnPostNotExist"/> is true, this method should return true if the post does not exist.
+ /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless <paramref name="throwOnPostNotExist"/> is true.
+ /// This method does not check whether the user is administrator.
+ /// It only checks whether he is the author of the post or the owner of the timeline.
+ /// Return false when user with modifier id does not exist.
+ /// </remarks>
+ Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
+ }
+
+ public class TimelinePostService : ITimelinePostService
+ {
+ private readonly ILogger<TimelinePostService> _logger;
+ private readonly DatabaseContext _database;
+ private readonly IBasicTimelineService _basicTimelineService;
+ private readonly IUserService _userService;
+ private readonly IDataManager _dataManager;
+ private readonly IImageValidator _imageValidator;
+ private readonly IClock _clock;
+
+ public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
+ {
+ _logger = logger;
+ _database = database;
+ _basicTimelineService = basicTimelineService;
+ _userService = userService;
+ _dataManager = dataManager;
+ _imageValidator = imageValidator;
+ _clock = clock;
+ }
+
+ private async Task<TimelinePostInfo> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
+ {
+ UserInfo? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null;
+
+ ITimelinePostContent? content = null;
+
+ if (entity.Content != null)
+ {
+ var type = entity.ContentType;
+
+ content = type switch
+ {
+ TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
+ TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
+ _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
+ };
+ }
+
+ return new TimelinePostInfo(
+ id: entity.LocalId,
+ author: author,
+ content: content,
+ time: entity.Time,
+ lastUpdated: entity.LastUpdated,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<List<TimelinePostInfo>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
+ {
+ modifiedSince = modifiedSince?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
+
+ if (!includeDeleted)
+ {
+ query = query.Where(p => p.Content != null);
+ }
+
+ if (modifiedSince.HasValue)
+ {
+ query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
+ }
+
+ query = query.OrderBy(p => p.Time);
+
+ var postEntities = await query.ToListAsync();
+
+ var posts = new List<TimelinePostInfo>();
+ foreach (var entity in postEntities)
+ {
+ posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
+ }
+ return posts;
+ }
+
+ public async Task<string> GetPostDataETag(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ return tag;
+ }
+
+ public async Task<PostData> GetPostData(string timelineName, long postId)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+
+ if (postEntity.Content == null)
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+
+ if (postEntity.ContentType != TimelinePostContentTypes.Image)
+ throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
+
+ var tag = postEntity.Content;
+
+ byte[] data;
+
+ try
+ {
+ data = await _dataManager.GetEntry(tag);
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
+ }
+
+ if (postEntity.ExtraContent == null)
+ {
+ _logger.LogWarning(LogGetDataNoFormat);
+ var format = Image.DetectFormat(data);
+ postEntity.ExtraContent = format.DefaultMimeType;
+ await _database.SaveChangesAsync();
+ }
+
+ return new PostData
+ {
+ Data = data,
+ Type = postEntity.ExtraContent,
+ ETag = tag,
+ LastModified = postEntity.LastUpdated
+ };
+ }
+
+ public async Task<TimelinePostInfo> CreateTextPost(string timelineName, long authorId, string text, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (text == null)
+ throw new ArgumentNullException(nameof(text));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(authorId);
+
+ var currentTime = _clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
+ {
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Text,
+ Content = text,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = finalTime,
+ LastUpdated = currentTime
+ };
+ _database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+
+ return new TimelinePostInfo(
+ id: postEntity.LocalId,
+ content: new TextTimelinePostContent(text),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task<TimelinePostInfo> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
+ {
+ time = time?.MyToUtc();
+
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
+
+ var author = await _userService.GetUser(authorId);
+
+ var imageFormat = await _imageValidator.Validate(data);
+
+ var imageFormatText = imageFormat.DefaultMimeType;
+
+ var tag = await _dataManager.RetainEntry(data);
+
+ var currentTime = _clock.GetCurrentTime();
+ var finalTime = time ?? currentTime;
+
+ timelineEntity.CurrentPostLocalId += 1;
+
+ var postEntity = new TimelinePostEntity
+ {
+ LocalId = timelineEntity.CurrentPostLocalId,
+ ContentType = TimelinePostContentTypes.Image,
+ Content = tag,
+ ExtraContent = imageFormatText,
+ AuthorId = authorId,
+ TimelineId = timelineId,
+ Time = finalTime,
+ LastUpdated = currentTime
+ };
+ _database.TimelinePosts.Add(postEntity);
+ await _database.SaveChangesAsync();
+
+ return new TimelinePostInfo(
+ id: postEntity.LocalId,
+ content: new ImageTimelinePostContent(tag),
+ time: finalTime,
+ author: author,
+ lastUpdated: currentTime,
+ timelineName: timelineName
+ );
+ }
+
+ public async Task DeletePost(string timelineName, long id)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
+
+ if (post == null)
+ throw new TimelinePostNotExistException(timelineName, id, false);
+
+ if (post.Content == null)
+ throw new TimelinePostNotExistException(timelineName, id, true);
+
+ string? dataTag = null;
+
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTag = post.Content;
+ }
+
+ post.Content = null;
+ post.LastUpdated = _clock.GetCurrentTime();
+
+ await _database.SaveChangesAsync();
+
+ if (dataTag != null)
+ {
+ await _dataManager.FreeEntry(dataTag);
+ }
+ }
+
+ public async Task DeleteAllPostsOfUser(long userId)
+ {
+ var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
+
+ var now = _clock.GetCurrentTime();
+
+ var dataTags = new List<string>();
+
+ foreach (var post in posts)
+ {
+ if (post.Content != null)
+ {
+ if (post.ContentType == TimelinePostContentTypes.Image)
+ {
+ dataTags.Add(post.Content);
+ }
+ post.Content = null;
+ }
+ post.LastUpdated = now;
+ }
+
+ await _database.SaveChangesAsync();
+
+ foreach (var dataTag in dataTags)
+ {
+ await _dataManager.FreeEntry(dataTag);
+ }
+ }
+
+ public async Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName);
+
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
+
+ var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
+
+ if (postEntity == null)
+ {
+ if (throwOnPostNotExist)
+ throw new TimelinePostNotExistException(timelineName, postId, false);
+ else
+ return true;
+ }
+
+ if (postEntity.Content == null && throwOnPostNotExist)
+ {
+ throw new TimelinePostNotExistException(timelineName, postId, true);
+ }
+
+ return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index 769e8bed..b65b3cf4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -1,13 +1,10 @@ using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
-using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
@@ -51,20 +48,10 @@ namespace Timeline.Services public long UserId { get; set; }
}
- public class PostData : ICacheableData
- {
-#pragma warning disable CA1819 // Properties should not return arrays
- public byte[] Data { get; set; } = default!;
-#pragma warning restore CA1819 // Properties should not return arrays
- public string Type { get; set; } = default!;
- public string ETag { get; set; } = default!;
- public DateTime? LastModified { get; set; } // TODO: Why nullable?
- }
-
/// <summary>
/// This define the interface of both personal timeline and ordinary timeline.
/// </summary>
- public interface ITimelineService
+ public interface ITimelineService : IBasicTimelineService
{
/// <summary>
/// Get the timeline last modified time (not include name change).
@@ -103,7 +90,15 @@ namespace Timeline.Services /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
/// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
/// </exception>
- Task<Models.Timeline> GetTimeline(string timelineName);
+ Task<TimelineInfo> GetTimeline(string timelineName);
+
+ /// <summary>
+ /// Get timeline by id.
+ /// </summary>
+ /// <param name="id">Id of timeline.</param>
+ /// <returns>The timeline.</returns>
+ /// <exception cref="TimelineNotExistException">Thrown when timeline with given id does not exist.</exception>
+ Task<TimelineInfo> GetTimelineById(long id);
/// <summary>
/// Set the properties of a timeline.
@@ -119,113 +114,6 @@ namespace Timeline.Services Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
/// <summary>
- /// Get all the posts in the timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="modifiedSince">The time that posts have been modified since.</param>
- /// <param name="includeDeleted">Whether include deleted posts.</param>
- /// <returns>A list of all posts.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
-
- /// <summary>
- /// Get the etag of data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostData(string, long)"/>
- Task<string> GetPostDataETag(string timelineName, long postId);
-
- /// <summary>
- /// Get the data of a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline of the post.</param>
- /// <param name="postId">The id of the post.</param>
- /// <returns>The etag of the data.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
- /// <exception cref="TimelinePostNoDataException">Thrown when post has no data.</exception>
- /// <seealso cref="GetPostDataETag(string, long)"/>
- Task<PostData> GetPostData(string timelineName, long postId);
-
- /// <summary>
- /// Create a new text post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="text">The content text.</param>
- /// <param name="time">The time of the post. If null, then current time is used.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="text"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
-
- /// <summary>
- /// Create a new image post in timeline.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to create post against.</param>
- /// <param name="authorId">The author's user id.</param>
- /// <param name="imageData">The image data.</param>
- /// <param name="time">The time of the post. If null, then use current time.</param>
- /// <returns>The info of the created post.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> or <paramref name="imageData"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="UserNotExistException">Thrown if user of <paramref name="authorId"/> does not exist.</exception>
- /// <exception cref="ImageException">Thrown if data is not a image. Validated by <see cref="ImageValidator"/>.</exception>
- Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time);
-
- /// <summary>
- /// Delete a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline to delete post against.</param>
- /// <param name="postId">The id of the post to delete.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already.</exception>
- /// <remarks>
- /// First use <see cref="HasPostModifyPermission(string, long, long, bool)"/> to check the permission.
- /// </remarks>
- Task DeletePost(string timelineName, long postId);
-
- /// <summary>
- /// Delete all posts of the given user. Used when delete a user.
- /// </summary>
- /// <param name="userId">The id of the user.</param>
- Task DeleteAllPostsOfUser(long userId);
-
- /// <summary>
/// Change member of timeline.
/// </summary>
/// <param name="timelineName">The name of the timeline.</param>
@@ -285,30 +173,6 @@ namespace Timeline.Services Task<bool> HasReadPermission(string timelineName, long? visitorId);
/// <summary>
- /// Verify whether a user has the permission to modify a post.
- /// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
- /// <param name="postId">The id of the post.</param>
- /// <param name="modifierId">The id of the user to check on.</param>
- /// <param name="throwOnPostNotExist">True if you want it to throw <see cref="TimelinePostNotExistException"/>. Default false.</param>
- /// <returns>True if can modify, false if can't modify.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
- /// <exception cref="ArgumentException">Throw when <paramref name="timelineName"/> is of bad format.</exception>
- /// <exception cref="TimelineNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// If it is a personal timeline, then inner exception is <see cref="UserNotExistException"/>.
- /// </exception>
- /// <exception cref="TimelinePostNotExistException">Thrown when the post with given id does not exist or is deleted already and <paramref name="throwOnPostNotExist"/> is true.</exception>
- /// <remarks>
- /// Unless <paramref name="throwOnPostNotExist"/> is true, this method should return true if the post does not exist.
- /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless <paramref name="throwOnPostNotExist"/> is true.
- /// This method does not check whether the user is administrator.
- /// It only checks whether he is the author of the post or the owner of the timeline.
- /// Return false when user with modifier id does not exist.
- /// </remarks>
- Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
-
- /// <summary>
/// Verify whether a user is member of a timeline.
/// </summary>
/// <param name="timelineName">The name of the timeline.</param>
@@ -335,7 +199,7 @@ namespace Timeline.Services /// <remarks>
/// If user with related user id does not exist, empty list will be returned.
/// </remarks>
- Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
+ Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null);
/// <summary>
/// Create a timeline.
@@ -347,7 +211,7 @@ namespace Timeline.Services /// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
/// <exception cref="EntityAlreadyExistException">Thrown when the timeline already exists.</exception>
/// <exception cref="UserNotExistException">Thrown when the owner user does not exist.</exception>
- Task<Models.Timeline> CreateTimeline(string timelineName, long ownerId);
+ Task<TimelineInfo> CreateTimeline(string timelineName, long ownerId);
/// <summary>
/// Delete a timeline.
@@ -371,31 +235,23 @@ namespace Timeline.Services /// <remarks>
/// You can only change name of general timeline.
/// </remarks>
- Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName);
+ Task<TimelineInfo> ChangeTimelineName(string oldTimelineName, string newTimelineName);
}
- public class TimelineService : ITimelineService
+ public class TimelineService : BasicTimelineService, ITimelineService
{
- public TimelineService(ILogger<TimelineService> logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock)
+ public TimelineService(DatabaseContext database, IUserService userService, IClock clock)
+ : base(database, userService, clock)
{
- _logger = logger;
_database = database;
- _dataManager = dataManager;
_userService = userService;
- _imageValidator = imageValidator;
_clock = clock;
}
- private readonly ILogger<TimelineService> _logger;
-
private readonly DatabaseContext _database;
- private readonly IDataManager _dataManager;
-
private readonly IUserService _userService;
- private readonly IImageValidator _imageValidator;
-
private readonly IClock _clock;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
@@ -411,11 +267,11 @@ namespace Timeline.Services }
/// Remember to include Members when query.
- private async Task<Models.Timeline> MapTimelineFromEntity(TimelineEntity entity)
+ private async Task<TimelineInfo> MapTimelineFromEntity(TimelineEntity entity)
{
var owner = await _userService.GetUser(entity.OwnerId);
- var members = new List<User>();
+ var members = new List<UserInfo>();
foreach (var memberEntity in entity.Members)
{
members.Add(await _userService.GetUser(memberEntity.UserId));
@@ -423,129 +279,18 @@ namespace Timeline.Services var name = entity.Name ?? ("@" + owner.Username);
- return new Models.Timeline
- {
- UniqueID = entity.UniqueId,
- Name = name,
- NameLastModified = entity.NameLastModified,
- Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title,
- Description = entity.Description ?? "",
- Owner = owner,
- Visibility = entity.Visibility,
- Members = members,
- CreateTime = entity.CreateTime,
- LastModified = entity.LastModified
- };
- }
-
- private async Task<TimelinePost> MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
- {
- User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null;
-
- ITimelinePostContent? content = null;
-
- if (entity.Content != null)
- {
- var type = entity.ContentType;
-
- content = type switch
- {
- TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
- TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
- _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
- };
- }
-
- return new TimelinePost(
- id: entity.LocalId,
- author: author,
- content: content,
- time: entity.Time,
- lastUpdated: entity.LastUpdated,
- timelineName: timelineName
- );
- }
-
- private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
- {
- var currentTime = _clock.GetCurrentTime();
-
- return new TimelineEntity
- {
- Name = name,
- NameLastModified = currentTime,
- OwnerId = ownerId,
- Visibility = TimelineVisibility.Register,
- CreateTime = currentTime,
- LastModified = currentTime,
- CurrentPostLocalId = 0,
- Members = new List<TimelineMemberEntity>()
- };
- }
-
-
-
- // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created.
- //
- // This method will check the name format and if it is invalid, ArgumentException is thrown.
- //
- // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception.
- // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown.
- //
- // It follows all timeline-related function common interface contracts.
- private async Task<long> FindTimelineId(string timelineName)
- {
- timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
-
- if (isPersonal)
- {
- long userId;
- try
- {
- userId = await _userService.GetUserIdByUsername(timelineName);
- }
- catch (ArgumentException e)
- {
- throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e);
- }
- catch (UserNotExistException e)
- {
- throw new TimelineNotExistException(timelineName, e);
- }
-
- var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
-
- if (timelineEntity != null)
- {
- return timelineEntity.Id;
- }
- else
- {
- var newTimelineEntity = CreateNewTimelineEntity(null, userId);
- _database.Timelines.Add(newTimelineEntity);
- await _database.SaveChangesAsync();
-
- return newTimelineEntity.Id;
- }
- }
- else
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- ValidateTimelineName(timelineName, nameof(timelineName));
-
- var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync();
-
- if (timelineEntity == null)
- {
- throw new TimelineNotExistException(timelineName);
- }
- else
- {
- return timelineEntity.Id;
- }
- }
+ return new TimelineInfo(
+ entity.UniqueId,
+ name,
+ entity.NameLastModified,
+ string.IsNullOrEmpty(entity.Title) ? name : entity.Title,
+ entity.Description ?? "",
+ owner,
+ entity.Visibility,
+ members,
+ entity.CreateTime,
+ entity.LastModified
+ );
}
public async Task<DateTime> GetTimelineLastModifiedTime(string timelineName)
@@ -553,7 +298,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
@@ -565,279 +310,33 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync();
return timelineEntity.UniqueId;
}
- public async Task<Models.Timeline> GetTimeline(string timelineName)
+ public async Task<TimelineInfo> GetTimeline(string timelineName)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync();
return await MapTimelineFromEntity(timelineEntity);
}
- public async Task<List<TimelinePost>> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
- {
- modifiedSince = modifiedSince?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
- IQueryable<TimelinePostEntity> query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
-
- if (!includeDeleted)
- {
- query = query.Where(p => p.Content != null);
- }
-
- if (modifiedSince.HasValue)
- {
- query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
- }
-
- query = query.OrderBy(p => p.Time);
-
- var postEntities = await query.ToListAsync();
-
- var posts = new List<TimelinePost>();
- foreach (var entity in postEntities)
- {
- posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
- }
- return posts;
- }
-
- public async Task<string> GetPostDataETag(string timelineName, long postId)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
-
- if (postEntity == null)
- throw new TimelinePostNotExistException(timelineName, postId, false);
-
- if (postEntity.Content == null)
- throw new TimelinePostNotExistException(timelineName, postId, true);
-
- if (postEntity.ContentType != TimelinePostContentTypes.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
-
- var tag = postEntity.Content;
-
- return tag;
- }
-
- public async Task<PostData> GetPostData(string timelineName, long postId)
+ public async Task<TimelineInfo> GetTimelineById(long id)
{
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
- var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
-
- if (postEntity == null)
- throw new TimelinePostNotExistException(timelineName, postId, false);
+ var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync();
- if (postEntity.Content == null)
- throw new TimelinePostNotExistException(timelineName, postId, true);
+ if (timelineEntity is null)
+ throw new TimelineNotExistException(id);
- if (postEntity.ContentType != TimelinePostContentTypes.Image)
- throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
-
- var tag = postEntity.Content;
-
- byte[] data;
-
- try
- {
- data = await _dataManager.GetEntry(tag);
- }
- catch (InvalidOperationException e)
- {
- throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
- }
-
- if (postEntity.ExtraContent == null)
- {
- _logger.LogWarning(LogGetDataNoFormat);
- var format = Image.DetectFormat(data);
- postEntity.ExtraContent = format.DefaultMimeType;
- await _database.SaveChangesAsync();
- }
-
- return new PostData
- {
- Data = data,
- Type = postEntity.ExtraContent,
- ETag = tag,
- LastModified = postEntity.LastUpdated
- };
- }
-
- public async Task<TimelinePost> CreateTextPost(string timelineName, long authorId, string text, DateTime? time)
- {
- time = time?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
- if (text == null)
- throw new ArgumentNullException(nameof(text));
-
- var timelineId = await FindTimelineId(timelineName);
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
-
- var author = await _userService.GetUser(authorId);
-
- var currentTime = _clock.GetCurrentTime();
- var finalTime = time ?? currentTime;
-
- timelineEntity.CurrentPostLocalId += 1;
-
- var postEntity = new TimelinePostEntity
- {
- LocalId = timelineEntity.CurrentPostLocalId,
- ContentType = TimelinePostContentTypes.Text,
- Content = text,
- AuthorId = authorId,
- TimelineId = timelineId,
- Time = finalTime,
- LastUpdated = currentTime
- };
- _database.TimelinePosts.Add(postEntity);
- await _database.SaveChangesAsync();
-
-
- return new TimelinePost(
- id: postEntity.LocalId,
- content: new TextTimelinePostContent(text),
- time: finalTime,
- author: author,
- lastUpdated: currentTime,
- timelineName: timelineName
- );
- }
-
- public async Task<TimelinePost> CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
- {
- time = time?.MyToUtc();
-
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
- if (data == null)
- throw new ArgumentNullException(nameof(data));
-
- var timelineId = await FindTimelineId(timelineName);
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
-
- var author = await _userService.GetUser(authorId);
-
- var imageFormat = await _imageValidator.Validate(data);
-
- var imageFormatText = imageFormat.DefaultMimeType;
-
- var tag = await _dataManager.RetainEntry(data);
-
- var currentTime = _clock.GetCurrentTime();
- var finalTime = time ?? currentTime;
-
- timelineEntity.CurrentPostLocalId += 1;
-
- var postEntity = new TimelinePostEntity
- {
- LocalId = timelineEntity.CurrentPostLocalId,
- ContentType = TimelinePostContentTypes.Image,
- Content = tag,
- ExtraContent = imageFormatText,
- AuthorId = authorId,
- TimelineId = timelineId,
- Time = finalTime,
- LastUpdated = currentTime
- };
- _database.TimelinePosts.Add(postEntity);
- await _database.SaveChangesAsync();
-
- return new TimelinePost(
- id: postEntity.LocalId,
- content: new ImageTimelinePostContent(tag),
- time: finalTime,
- author: author,
- lastUpdated: currentTime,
- timelineName: timelineName
- );
- }
-
- public async Task DeletePost(string timelineName, long id)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
-
- if (post == null)
- throw new TimelinePostNotExistException(timelineName, id, false);
-
- if (post.Content == null)
- throw new TimelinePostNotExistException(timelineName, id, true);
-
- string? dataTag = null;
-
- if (post.ContentType == TimelinePostContentTypes.Image)
- {
- dataTag = post.Content;
- }
-
- post.Content = null;
- post.LastUpdated = _clock.GetCurrentTime();
-
- await _database.SaveChangesAsync();
-
- if (dataTag != null)
- {
- await _dataManager.FreeEntry(dataTag);
- }
- }
-
- public async Task DeleteAllPostsOfUser(long userId)
- {
- var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync();
-
- var now = _clock.GetCurrentTime();
-
- var dataTags = new List<string>();
-
- foreach (var post in posts)
- {
- if (post.Content != null)
- {
- if (post.ContentType == TimelinePostContentTypes.Image)
- {
- dataTags.Add(post.Content);
- }
- post.Content = null;
- }
- post.LastUpdated = now;
- }
-
- await _database.SaveChangesAsync();
-
- foreach (var dataTag in dataTags)
- {
- await _dataManager.FreeEntry(dataTag);
- }
+ return await MapTimelineFromEntity(timelineEntity);
}
public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
@@ -847,7 +346,7 @@ namespace Timeline.Services if (newProperties == null)
throw new ArgumentNullException(nameof(newProperties));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
@@ -933,7 +432,7 @@ namespace Timeline.Services if (simplifiedAdd == null && simplifiedRemove == null)
return;
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
async Task<List<long>?> CheckExistenceAndGetId(List<string>? list)
{
@@ -973,7 +472,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
return userId == timelineEntity.OwnerId;
@@ -984,7 +483,7 @@ namespace Timeline.Services if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
if (timelineEntity.Visibility == TimelineVisibility.Public)
@@ -1004,39 +503,12 @@ namespace Timeline.Services }
}
- public async Task<bool> HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- var timelineId = await FindTimelineId(timelineName);
-
- var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
-
- var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync();
-
- if (postEntity == null)
- {
- if (throwOnPostNotExist)
- throw new TimelinePostNotExistException(timelineName, postId, false);
- else
- return true;
- }
-
- if (postEntity.Content == null && throwOnPostNotExist)
- {
- throw new TimelinePostNotExistException(timelineName, postId, true);
- }
-
- return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
- }
-
public async Task<bool> IsMemberOf(string timelineName, long userId)
{
if (timelineName == null)
throw new ArgumentNullException(nameof(timelineName));
- var timelineId = await FindTimelineId(timelineName);
+ var timelineId = await GetTimelineIdByName(timelineName);
var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
@@ -1046,7 +518,7 @@ namespace Timeline.Services return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
}
- public async Task<List<Models.Timeline>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
+ public async Task<List<TimelineInfo>> GetTimelines(TimelineUserRelationship? relate = null, List<TimelineVisibility>? visibility = null)
{
List<TimelineEntity> entities;
@@ -1080,7 +552,7 @@ namespace Timeline.Services }
}
- var result = new List<Models.Timeline>();
+ var result = new List<TimelineInfo>();
foreach (var entity in entities)
{
@@ -1090,7 +562,7 @@ namespace Timeline.Services return result;
}
- public async Task<Models.Timeline> CreateTimeline(string name, long owner)
+ public async Task<TimelineInfo> CreateTimeline(string name, long owner)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
@@ -1128,7 +600,7 @@ namespace Timeline.Services await _database.SaveChangesAsync();
}
- public async Task<Models.Timeline> ChangeTimelineName(string oldTimelineName, string newTimelineName)
+ public async Task<TimelineInfo> ChangeTimelineName(string oldTimelineName, string newTimelineName)
{
if (oldTimelineName == null)
throw new ArgumentNullException(nameof(oldTimelineName));
diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs new file mode 100644 index 00000000..8aeef9ef --- /dev/null +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Helpers;
+using Timeline.Models.Validation;
+using Timeline.Services.Exceptions;
+
+namespace Timeline.Services
+{
+ public interface IUserCredentialService
+ {
+ /// <summary>
+ /// Try to verify the given username and password.
+ /// </summary>
+ /// <param name="username">The username of the user to verify.</param>
+ /// <param name="password">The password of the user to verify.</param>
+ /// <returns>User id.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or <paramref name="password"/> is empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
+ Task<long> VerifyCredential(string username, string password);
+
+ /// <summary>
+ /// Try to change a user's password with old password.
+ /// </summary>
+ /// <param name="id">The id of user to change password of.</param>
+ /// <param name="oldPassword">Old password.</param>
+ /// <param name="newPassword">New password.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
+ Task ChangePassword(long id, string oldPassword, string newPassword);
+ }
+
+ public class UserCredentialService : IUserCredentialService
+ {
+ private readonly ILogger<UserCredentialService> _logger;
+ private readonly DatabaseContext _database;
+ private readonly IPasswordService _passwordService;
+
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
+
+ public UserCredentialService(ILogger<UserCredentialService> logger, DatabaseContext database, IPasswordService passwordService)
+ {
+ _logger = logger;
+ _database = database;
+ _passwordService = passwordService;
+ }
+
+ public async Task<long> VerifyCredential(string username, string password)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+ if (!_usernameValidator.Validate(username, out var message))
+ throw new ArgumentException(message);
+ if (password.Length == 0)
+ throw new ArgumentException("Password can't be empty.");
+
+ var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(username);
+
+ if (!_passwordService.VerifyPassword(entity.Password, password))
+ throw new BadPasswordException(password);
+
+ return entity.Id;
+ }
+
+ public async Task ChangePassword(long id, string oldPassword, string newPassword)
+ {
+ if (oldPassword == null)
+ throw new ArgumentNullException(nameof(oldPassword));
+ if (newPassword == null)
+ throw new ArgumentNullException(nameof(newPassword));
+ if (oldPassword.Length == 0)
+ throw new ArgumentException("Old password can't be empty.");
+ if (newPassword.Length == 0)
+ throw new ArgumentException("New password can't be empty.");
+
+ var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+
+ if (entity == null)
+ throw new UserNotExistException(id);
+
+ if (!_passwordService.VerifyPassword(entity.Password, oldPassword))
+ throw new BadPasswordException(oldPassword);
+
+ entity.Password = _passwordService.HashPassword(newPassword);
+ entity.Version += 1;
+ await _database.SaveChangesAsync();
+ _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs index 5365313b..a4e77abc 100644 --- a/BackEnd/Timeline/Services/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -31,15 +31,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext;
- private readonly ITimelineService _timelineService;
+ private readonly ITimelinePostService _timelinePostService;
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
- public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelineService timelineService)
+ public UserDeleteService(ILogger<UserDeleteService> logger, DatabaseContext databaseContext, ITimelinePostService timelinePostService)
{
_logger = logger;
_databaseContext = databaseContext;
- _timelineService = timelineService;
+ _timelinePostService = timelinePostService;
}
public async Task<bool> DeleteUser(string username)
@@ -59,7 +59,7 @@ namespace Timeline.Services if (user.Id == 1)
throw new InvalidOperationOnRootUserException("Can't delete root user.");
- await _timelineService.DeleteAllPostsOfUser(user.Id);
+ await _timelinePostService.DeleteAllPostsOfUser(user.Id);
_databaseContext.Users.Remove(user);
diff --git a/BackEnd/Timeline/Services/UserPermissionService.cs b/BackEnd/Timeline/Services/UserPermissionService.cs index 9683000a..bd7cd6aa 100644 --- a/BackEnd/Timeline/Services/UserPermissionService.cs +++ b/BackEnd/Timeline/Services/UserPermissionService.cs @@ -28,7 +28,7 @@ namespace Timeline.Services /// <summary>
/// Represents a user's permissions.
/// </summary>
- public class UserPermissions : IEnumerable<UserPermission>
+ public class UserPermissions : IEnumerable<UserPermission>, IEquatable<UserPermissions>
{
public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues<UserPermission>());
@@ -49,10 +49,10 @@ namespace Timeline.Services public UserPermissions(IEnumerable<UserPermission> permissions)
{
if (permissions == null) throw new ArgumentNullException(nameof(permissions));
- _permissions = new HashSet<UserPermission>(permissions);
+ _permissions = new SortedSet<UserPermission>(permissions);
}
- private readonly HashSet<UserPermission> _permissions = new();
+ private readonly SortedSet<UserPermission> _permissions = new();
/// <summary>
/// Check if a permission is contained in the list.
@@ -108,6 +108,33 @@ namespace Timeline.Services {
return ((IEnumerable)_permissions).GetEnumerator();
}
+
+ public bool Equals(UserPermissions? other)
+ {
+ if (other == null)
+ return false;
+
+ return _permissions.SequenceEqual(other._permissions);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as UserPermissions);
+ }
+
+ public override int GetHashCode()
+ {
+ int result = 0;
+ foreach (var permission in Enum.GetValues<UserPermission>())
+ {
+ if (_permissions.Contains(permission))
+ {
+ result += 1;
+ }
+ result <<= 1;
+ }
+ return result;
+ }
}
public interface IUserPermissionService
diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 2c5644cd..c99e86b0 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -17,50 +17,28 @@ namespace Timeline.Services /// <summary>
/// Null means not change.
/// </summary>
- public record ModifyUserParams
+ public class ModifyUserParams
{
public string? Username { get; set; }
public string? Password { get; set; }
public string? Nickname { get; set; }
}
- public interface IUserService
+ public interface IUserService : IBasicUserService
{
/// <summary>
- /// Try to verify the given username and password.
- /// </summary>
- /// <param name="username">The username of the user to verify.</param>
- /// <param name="password">The password of the user to verify.</param>
- /// <returns>The user info and auth info.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format or <paramref name="password"/> is empty.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<User> VerifyCredential(string username, string password);
-
- /// <summary>
/// Try to get a user by id.
/// </summary>
/// <param name="id">The id of the user.</param>
/// <returns>The user info.</returns>
/// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
- Task<User> GetUser(long id);
-
- /// <summary>
- /// Get the user id of given username.
- /// </summary>
- /// <param name="username">Username of the user.</param>
- /// <returns>The id of the user.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown when <paramref name="username"/> is of bad format.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<long> GetUserIdByUsername(string username);
+ Task<UserInfo> GetUser(long id);
/// <summary>
/// List all users.
/// </summary>
/// <returns>The user info of users.</returns>
- Task<List<User>> GetUsers();
+ Task<List<UserInfo>> GetUsers();
/// <summary>
/// Create a user with given info.
@@ -71,7 +49,7 @@ namespace Timeline.Services /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="username"/> or <paramref name="password"/> is of bad format.</exception>
/// <exception cref="EntityAlreadyExistException">Thrown when a user with given username already exists.</exception>
- Task<User> CreateUser(string username, string password);
+ Task<UserInfo> CreateUser(string username, string password);
/// <summary>
/// Modify a user.
@@ -84,22 +62,10 @@ namespace Timeline.Services /// <remarks>
/// Version will increase if password is changed.
/// </remarks>
- Task<User> ModifyUser(long id, ModifyUserParams? param);
-
- /// <summary>
- /// Try to change a user's password with old password.
- /// </summary>
- /// <param name="id">The id of user to change password of.</param>
- /// <param name="oldPassword">Old password.</param>
- /// <param name="newPassword">New password.</param>
- /// <exception cref="ArgumentNullException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
- /// <exception cref="ArgumentException">Thrown if <paramref name="oldPassword"/> or <paramref name="newPassword"/> is empty.</exception>
- /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
- /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
- Task ChangePassword(long id, string oldPassword, string newPassword);
+ Task<UserInfo> ModifyUser(long id, ModifyUserParams? param);
}
- public class UserService : IUserService
+ public class UserService : BasicUserService, IUserService
{
private readonly ILogger<UserService> _logger;
private readonly IClock _clock;
@@ -112,13 +78,13 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator();
private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService)
+ public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserPermissionService userPermissionService, IClock clock) : base(databaseContext)
{
_logger = logger;
- _clock = clock;
_databaseContext = databaseContext;
_passwordService = passwordService;
_userPermissionService = userPermissionService;
+ _clock = clock;
}
private void CheckUsernameFormat(string username, string? paramName)
@@ -150,45 +116,23 @@ namespace Timeline.Services throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict);
}
- private async Task<User> CreateUserFromEntity(UserEntity entity)
+ private async Task<UserInfo> CreateUserFromEntity(UserEntity entity)
{
var permission = await _userPermissionService.GetPermissionsOfUserAsync(entity.Id);
- return new User
- {
- UniqueId = entity.UniqueId,
- Username = entity.Username,
- Permissions = permission,
- Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
- Id = entity.Id,
- Version = entity.Version,
- CreateTime = entity.CreateTime,
- UsernameChangeTime = entity.UsernameChangeTime,
- LastModified = entity.LastModified
- };
- }
-
- public async Task<User> VerifyCredential(string username, string password)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- if (password == null)
- throw new ArgumentNullException(nameof(password));
-
- CheckUsernameFormat(username, nameof(username));
- CheckPasswordFormat(password, nameof(password));
-
- var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync();
-
- if (entity == null)
- throw new UserNotExistException(username);
-
- if (!_passwordService.VerifyPassword(entity.Password, password))
- throw new BadPasswordException(password);
-
- return await CreateUserFromEntity(entity);
+ return new UserInfo(
+ entity.Id,
+ entity.UniqueId,
+ entity.Username,
+ string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname,
+ permission,
+ entity.UsernameChangeTime,
+ entity.CreateTime,
+ entity.LastModified,
+ entity.Version
+ );
}
- public async Task<User> GetUser(long id)
+ public async Task<UserInfo> GetUser(long id)
{
var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
@@ -198,24 +142,9 @@ namespace Timeline.Services return await CreateUserFromEntity(user);
}
- public async Task<long> GetUserIdByUsername(string username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- CheckUsernameFormat(username, nameof(username));
-
- var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
-
- if (entity == null)
- throw new UserNotExistException(username);
-
- return entity.Id;
- }
-
- public async Task<List<User>> GetUsers()
+ public async Task<List<UserInfo>> GetUsers()
{
- List<User> result = new();
+ List<UserInfo> result = new();
foreach (var entity in await _databaseContext.Users.ToArrayAsync())
{
result.Add(await CreateUserFromEntity(entity));
@@ -223,7 +152,7 @@ namespace Timeline.Services return result;
}
- public async Task<User> CreateUser(string username, string password)
+ public async Task<UserInfo> CreateUser(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
@@ -251,7 +180,7 @@ namespace Timeline.Services return await CreateUserFromEntity(newEntity);
}
- public async Task<User> ModifyUser(long id, ModifyUserParams? param)
+ public async Task<UserInfo> ModifyUser(long id, ModifyUserParams? param)
{
if (param != null)
{
@@ -311,28 +240,5 @@ namespace Timeline.Services return await CreateUserFromEntity(entity);
}
-
- public async Task ChangePassword(long id, string oldPassword, string newPassword)
- {
- if (oldPassword == null)
- throw new ArgumentNullException(nameof(oldPassword));
- if (newPassword == null)
- throw new ArgumentNullException(nameof(newPassword));
- CheckPasswordFormat(oldPassword, nameof(oldPassword));
- CheckPasswordFormat(newPassword, nameof(newPassword));
-
- var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
-
- if (entity == null)
- throw new UserNotExistException(id);
-
- if (!_passwordService.VerifyPassword(entity.Password, oldPassword))
- throw new BadPasswordException(oldPassword);
-
- entity.Password = _passwordService.HashPassword(newPassword);
- entity.Version += 1;
- await _databaseContext.SaveChangesAsync();
- _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password")));
- }
}
}
diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 09ecd19c..b887b987 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -10,7 +10,7 @@ namespace Timeline.Services public class UserTokenCreateResult
{
public string Token { get; set; } = default!;
- public User User { get; set; } = default!;
+ public UserInfo User { get; set; } = default!;
}
public interface IUserTokenManager
@@ -38,20 +38,22 @@ namespace Timeline.Services /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
/// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
- public Task<User> VerifyToken(string token);
+ public Task<UserInfo> VerifyToken(string token);
}
public class UserTokenManager : IUserTokenManager
{
private readonly ILogger<UserTokenManager> _logger;
private readonly IUserService _userService;
+ private readonly IUserCredentialService _userCredentialService;
private readonly IUserTokenService _userTokenService;
private readonly IClock _clock;
- public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
+ public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserCredentialService userCredentialService, IUserTokenService userTokenService, IClock clock)
{
_logger = logger;
_userService = userService;
+ _userCredentialService = userCredentialService;
_userTokenService = userTokenService;
_clock = clock;
}
@@ -65,14 +67,15 @@ namespace Timeline.Services if (password == null)
throw new ArgumentNullException(nameof(password));
- var user = await _userService.VerifyCredential(username, password);
+ var userId = await _userCredentialService.VerifyCredential(username, password);
+ var user = await _userService.GetUser(userId);
var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt });
return new UserTokenCreateResult { Token = token, User = user };
}
- public async Task<User> VerifyToken(string token)
+ public async Task<UserInfo> VerifyToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 532c63d0..d20fc54b 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -72,34 +72,36 @@ namespace Timeline services.AddAuthentication(AuthenticationConstants.Scheme)
.AddScheme<MyAuthenticationOptions, MyAuthenticationHandler>(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { });
services.AddAuthorization();
-
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
- services.AddSingleton<IPathProvider, PathProvider>();
+ services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
+ services.AddSingleton<IPathProvider, PathProvider>();
services.AddSingleton<IDatabaseBackupService, DatabaseBackupService>();
services.AddAutoMapper(GetType().Assembly);
services.AddTransient<IClock, Clock>();
+ services.AddScoped<IETagGenerator, ETagGenerator>();
+ services.AddScoped<IDataManager, DataManager>();
+ services.AddScoped<IImageValidator, ImageValidator>();
+
services.AddTransient<IPasswordService, PasswordService>();
+ services.AddScoped<IBasicUserService, BasicUserService>();
services.AddScoped<IUserService, UserService>();
+ services.AddScoped<IUserCredentialService, UserCredentialService>();
services.AddScoped<IUserDeleteService, UserDeleteService>();
services.AddScoped<IUserTokenService, JwtUserTokenService>();
services.AddScoped<IUserTokenManager, UserTokenManager>();
services.AddScoped<IUserPermissionService, UserPermissionService>();
-
- services.AddScoped<IETagGenerator, ETagGenerator>();
- services.AddScoped<IDataManager, DataManager>();
-
- services.AddScoped<IImageValidator, ImageValidator>();
-
services.AddUserAvatarService();
+ services.AddScoped<IBasicTimelineService, BasicTimelineService>();
services.AddScoped<ITimelineService, TimelineService>();
+ services.AddScoped<ITimelinePostService, TimelinePostService>();
- services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
+ services.AddScoped<IHighlightTimelineService, HighlightTimelineService>();
services.AddDbContext<DatabaseContext>((services, options) =>
{
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj index 0cb1b5ba..70536e00 100644 --- a/BackEnd/Timeline/Timeline.csproj +++ b/BackEnd/Timeline/Timeline.csproj @@ -45,8 +45,8 @@ <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="NSwag.AspNetCore" Version="13.8.2" />
- <PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
+ <PackageReference Include="NSwag.AspNetCore" Version="13.9.4" />
+ <PackageReference Include="SixLabors.ImageSharp" Version="1.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
</ItemGroup>
diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json index 36fdbcf2..36442da7 100644 --- a/BackEnd/Timeline/packages.lock.json +++ b/BackEnd/Timeline/packages.lock.json @@ -87,28 +87,28 @@ },
"NSwag.AspNetCore": {
"type": "Direct",
- "requested": "[13.8.2, )",
- "resolved": "13.8.2",
- "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==",
+ "requested": "[13.9.4, )",
+ "resolved": "13.9.4",
+ "contentHash": "2+QqWsUMfwOy1pFsacA4hjVW0tmKrcGG4O1JCVhM93q7QfqbG/ndt002a/GGGyaMsuK7HRKmYJ8nmo5tzOE1tg==",
"dependencies": {
"Microsoft.AspNetCore.Mvc.Core": "1.0.4",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
"Microsoft.AspNetCore.StaticFiles": "1.0.4",
"Microsoft.Extensions.ApiDescription.Server": "3.0.0",
"Microsoft.Extensions.FileProviders.Embedded": "1.0.1",
- "NSwag.Annotations": "13.8.2",
- "NSwag.Core": "13.8.2",
- "NSwag.Generation": "13.8.2",
- "NSwag.Generation.AspNetCore": "13.8.2",
+ "NSwag.Annotations": "13.9.4",
+ "NSwag.Core": "13.9.4",
+ "NSwag.Generation": "13.9.4",
+ "NSwag.Generation.AspNetCore": "13.9.4",
"System.IO.FileSystem": "4.3.0",
"System.Xml.XPath.XDocument": "4.0.1"
}
},
"SixLabors.ImageSharp": {
"type": "Direct",
- "requested": "[1.0.1, )",
- "resolved": "1.0.1",
- "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ=="
+ "requested": "[1.0.2, )",
+ "resolved": "1.0.2",
+ "contentHash": "iZJ37Iss3pUkFl961x1aka85QuvgY9oNZabHijzVnHs4QTz6EMNx3zjJDyvK/0+Ryj6JPv/PC7GVIJXLHtu2nQ=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Direct",
@@ -648,8 +648,8 @@ },
"NJsonSchema": {
"type": "Transitive",
- "resolved": "10.2.1",
- "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==",
+ "resolved": "10.3.1",
+ "contentHash": "k5ptrRSxMy1lZXxU7dXW2Gy9Q7uPufSGtb609tfuFdo+w45UMHdBolvbWeEq482BPXhYfoBZ2uNzjJgcny2o3g==",
"dependencies": {
"Namotion.Reflection": "1.0.14",
"Newtonsoft.Json": "9.0.1"
@@ -657,38 +657,38 @@ },
"NSwag.Annotations": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q=="
+ "resolved": "13.9.4",
+ "contentHash": "qsOYnNMUJJ5VpgYmQsyNkDKbJnMaRo4lGBgkaBlZsHsWGG+HizNkx+HuHkRtI0ks28jqZXpVDxDmnuyq/SwFnw=="
},
"NSwag.Core": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==",
+ "resolved": "13.9.4",
+ "contentHash": "iNhgBGWT5yEYL3uV6Xhla+VspVaN3NfDi+rjDugWLErU+A7uxV71D1i9OQkW37rdOFKiaixJAXPaLA6JA8c8hw==",
"dependencies": {
- "NJsonSchema": "10.2.1",
+ "NJsonSchema": "10.3.1",
"Newtonsoft.Json": "9.0.1"
}
},
"NSwag.Generation": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==",
+ "resolved": "13.9.4",
+ "contentHash": "Y6qqOYUEoYZRL5nYshzYn0b7Nz9Rzr6qCdVkah6mQq39Pom/XQgygaV6JR3t0dacDYg/XmVsMn++bdLPQs9rAw==",
"dependencies": {
- "NJsonSchema": "10.2.1",
- "NSwag.Core": "13.8.2",
+ "NJsonSchema": "10.3.1",
+ "NSwag.Core": "13.9.4",
"Newtonsoft.Json": "9.0.1"
}
},
"NSwag.Generation.AspNetCore": {
"type": "Transitive",
- "resolved": "13.8.2",
- "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==",
+ "resolved": "13.9.4",
+ "contentHash": "N0HGoPJsK67GAtNnPln0MLPnmv9wVp9Ev5sfEuWQIa/VHMkXQL6IyvItXiigDLb8VHXnqUbggU7WBg9Ay6h8oQ==",
"dependencies": {
"Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4",
"Microsoft.AspNetCore.Mvc.Core": "1.0.4",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4",
- "NJsonSchema": "10.2.1",
- "NSwag.Generation": "13.8.2"
+ "NJsonSchema": "10.3.1",
+ "NSwag.Generation": "13.9.4"
}
},
"runtime.native.System": {
|