aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs1
-rw-r--r--BackEnd/Timeline.Tests/Services/SearchServiceTest.cs6
-rw-r--r--BackEnd/Timeline.Tests/Services/ServiceTestBase.cs2
-rw-r--r--BackEnd/Timeline.Tests/Timeline.Tests.csproj2
-rw-r--r--BackEnd/Timeline.Tests/packages.lock.json12
-rw-r--r--BackEnd/Timeline/Controllers/TimelineController.cs12
-rw-r--r--BackEnd/Timeline/Controllers/TimelineV2Controller.cs33
-rw-r--r--BackEnd/Timeline/ErrorCodes.cs1
-rw-r--r--BackEnd/Timeline/Filters/CatchMultipleTimelineExceptionAttribute.cs20
-rw-r--r--BackEnd/Timeline/Filters/Resource.Designer.cs258
-rw-r--r--BackEnd/Timeline/Filters/Resource.resx3
-rw-r--r--BackEnd/Timeline/Models/Validation/NameValidator.cs10
-rw-r--r--BackEnd/Timeline/Models/Validation/Resource.Designer.cs312
-rw-r--r--BackEnd/Timeline/Models/Validation/Resource.resx3
-rw-r--r--BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs7
-rw-r--r--BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs6
-rw-r--r--BackEnd/Timeline/Services/Api/HighlightTimelineService.cs6
-rw-r--r--BackEnd/Timeline/Services/EntityType.cs25
-rw-r--r--BackEnd/Timeline/Services/Timeline/BasicTimelineService.cs131
-rw-r--r--BackEnd/Timeline/Services/Timeline/BasicTimelineServiceExtensions.cs17
-rw-r--r--BackEnd/Timeline/Services/Timeline/IBasicTimelineService.cs30
-rw-r--r--BackEnd/Timeline/Services/Timeline/ITimelineService.cs48
-rw-r--r--BackEnd/Timeline/Services/Timeline/MultipleTimelineException.cs44
-rw-r--r--BackEnd/Timeline/Services/Timeline/TimelinePostService.cs6
-rw-r--r--BackEnd/Timeline/Services/Timeline/TimelineService.cs174
-rw-r--r--BackEnd/Timeline/Services/Timeline/TimelineServiceExtensions.cs11
-rw-r--r--BackEnd/Timeline/Services/Timeline/TimelineServicesServiceCollectionExtensions.cs1
-rw-r--r--BackEnd/Timeline/Services/User/Avatar/UserAvatarService.cs14
-rw-r--r--BackEnd/Timeline/Services/User/BasicUserService.cs65
-rw-r--r--BackEnd/Timeline/Services/User/CreateTokenResult.cs12
-rw-r--r--BackEnd/Timeline/Services/User/IBasicUserService.cs36
-rw-r--r--BackEnd/Timeline/Services/User/IUserService.cs29
-rw-r--r--BackEnd/Timeline/Services/User/UserPermissionService.cs8
-rw-r--r--BackEnd/Timeline/Services/User/UserService.cs71
-rw-r--r--BackEnd/Timeline/Services/User/UserServiceExtensions.cs (renamed from BackEnd/Timeline/Services/User/BasicUserServiceExtensions.cs)4
-rw-r--r--BackEnd/Timeline/Services/User/UserServicesServiceCollectionExtensions.cs1
-rw-r--r--BackEnd/Timeline/Timeline.csproj2
-rw-r--r--BackEnd/Timeline/packages.lock.json6
-rw-r--r--FrontEnd/package.json2
39 files changed, 768 insertions, 663 deletions
diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
index d7832b60..72553248 100644
--- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
+++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs
@@ -383,7 +383,6 @@ namespace Timeline.Tests.IntegratedTests
using (var client = await CreateClientAsUser())
{
await client.TestPatchAssertInvalidModelAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "!!!" });
- await client.TestPatchAssertErrorAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "t2" }, errorCode: ErrorCodes.Conflict.Timeline);
await client.TestPatchAsync("timelines/t1", new HttpTimelinePatchRequest { Name = "newt" });
diff --git a/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs b/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs
index 6e3c0f40..9195ef4f 100644
--- a/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs
+++ b/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs
@@ -19,10 +19,10 @@ namespace Timeline.Tests.Services
[Fact]
public async Task TimelineSearch_Should_Work()
{
- await TimelineService.CreateTimelineAsync("hahaha", UserId);
- var t2 = await TimelineService.CreateTimelineAsync("bababa", UserId);
+ await TimelineService.CreateTimelineAsync(UserId, "hahaha");
+ var t2 = await TimelineService.CreateTimelineAsync(UserId, "bababa");
await TimelineService.ChangePropertyAsync(t2.Id, new TimelineChangePropertyParams { Title = "hahaha" });
- await TimelineService.CreateTimelineAsync("bbbbbb", UserId);
+ await TimelineService.CreateTimelineAsync(UserId, "bbbbbb");
var searchResult = await _service.SearchTimelineAsync("hah");
searchResult.Items.Should().HaveCount(2);
diff --git a/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs
index fea31d0a..039d3f59 100644
--- a/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs
+++ b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs
@@ -43,7 +43,7 @@ namespace Timeline.Tests.Services
UserTokenService = UserTokenServiceMock.Object;
UserService = new UserService(NullLogger<UserService>.Instance, Database, new PasswordService(), UserTokenService, Clock);
- TimelineService = new TimelineService(NullLoggerFactory.Instance, Database, UserService, Clock);
+ TimelineService = new TimelineService(NullLogger<TimelineService>.Instance, Database, UserService, Clock);
UserId = await UserService.GetUserIdByUsernameAsync("user");
AdminId = await UserService.GetUserIdByUsernameAsync("admin");
diff --git a/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
index b699eb49..82dc4d6a 100644
--- a/BackEnd/Timeline.Tests/Timeline.Tests.csproj
+++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj
@@ -12,7 +12,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="FluentAssertions" Version="6.5.1" />
+ <PackageReference Include="FluentAssertions" Version="6.6.0" />
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.2.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json
index 6098e0ba..5c71f593 100644
--- a/BackEnd/Timeline.Tests/packages.lock.json
+++ b/BackEnd/Timeline.Tests/packages.lock.json
@@ -10,9 +10,9 @@
},
"FluentAssertions": {
"type": "Direct",
- "requested": "[6.5.1, )",
- "resolved": "6.5.1",
- "contentHash": "hdY/upTFYEc7SFJQ+pKsMZP1J5bELh9feB8kXzk6FKAGE/tvAPDzHd76Z+nIYdgnqkocthaDUq+tKPsGD0um2A==",
+ "requested": "[6.6.0, )",
+ "resolved": "6.6.0",
+ "contentHash": "gBsgPrNRkzUQfnxZSKnU0oVILIc5dr+dmdKXscyYKD5URcwNVQ72a7uuCvTyBzRZW98MZQNolSYC0y/MQTJ03A==",
"dependencies": {
"System.Configuration.ConfigurationManager": "4.4.0"
}
@@ -130,8 +130,8 @@
},
"Markdig": {
"type": "Transitive",
- "resolved": "0.28.0",
- "contentHash": "gT6Sm14OaKQDwus5wFoI/v+Zxrgd2r70pkfq6dC1mIZaK1rvdEHtfnf3nD/Q/clrPFcvBIxZag79NXFjMkxhmA=="
+ "resolved": "0.28.1",
+ "contentHash": "70CneXw2N/1t7v6OfZJqMKLPRB1YWTPddEIcHT/P6IL6X1zsXELIu/DHVt96kr83PIVLznMuXoFK6b9N9KTODg=="
},
"Microsoft.AspNetCore.Connections.Abstractions": {
"type": "Transitive",
@@ -1755,7 +1755,7 @@
"dependencies": {
"AutoMapper": "11.0.1",
"AutoMapper.Extensions.Microsoft.DependencyInjection": "11.0.0",
- "Markdig": "0.28.0",
+ "Markdig": "0.28.1",
"Microsoft.AspNetCore.SpaServices.Extensions": "6.0.3",
"Microsoft.EntityFrameworkCore": "6.0.3",
"Microsoft.EntityFrameworkCore.Analyzers": "6.0.3",
diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs
index 42b8f210..7aeec02f 100644
--- a/BackEnd/Timeline/Controllers/TimelineController.cs
+++ b/BackEnd/Timeline/Controllers/TimelineController.cs
@@ -21,6 +21,7 @@ namespace Timeline.Controllers
/// </summary>
[ApiController]
[Route("timelines")]
+ [CatchMultipleTimelineException]
[ProducesErrorResponseType(typeof(CommonResponse))]
public class TimelineController : MyControllerBase
{
@@ -117,12 +118,13 @@ namespace Timeline.Controllers
/// <returns>The timeline info.</returns>
[HttpGet("{timeline}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<HttpTimeline>> TimelineGet([FromRoute][GeneralTimelineName] string timeline)
- {
- var timelineId = await _service.GetTimelineIdByNameAsync(timeline);
- var t = await _service.GetTimelineAsync(timelineId);
- var result = await Map(t);
+ {
+ var timelineId = await _service.GetTimelineIdByNameAsync(timeline);
+ var t = await _service.GetTimelineAsync(timelineId);
+ var result = await Map(t);
return result;
}
@@ -218,7 +220,7 @@ namespace Timeline.Controllers
{
var userId = GetAuthUserId();
- var timeline = await _service.CreateTimelineAsync(body.Name, userId);
+ var timeline = await _service.CreateTimelineAsync(userId, body.Name);
var result = await Map(timeline);
return result;
}
diff --git a/BackEnd/Timeline/Controllers/TimelineV2Controller.cs b/BackEnd/Timeline/Controllers/TimelineV2Controller.cs
new file mode 100644
index 00000000..7543c2a8
--- /dev/null
+++ b/BackEnd/Timeline/Controllers/TimelineV2Controller.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Timeline.Entities;
+using Timeline.Models.Http;
+using Timeline.Services.Mapper;
+using Timeline.Services.Timeline;
+
+namespace Timeline.Controllers
+{
+ [ApiController]
+ [Route("v2/timelines")]
+ public class TimelineV2Controller : MyControllerBase
+ {
+ private ITimelineService _timelineService;
+ private TimelineMapper _timelineMapper;
+
+ public TimelineV2Controller(ITimelineService timelineService, TimelineMapper timelineMapper)
+ {
+ _timelineService = timelineService;
+ _timelineMapper = timelineMapper;
+ }
+
+ [HttpGet("{owner}/{timeline}")]
+ public async Task<ActionResult<HttpTimeline>> Get([FromRoute] string owner, [FromRoute] string timeline)
+ {
+ var timelineId = await _timelineService.GetTimelineIdAsync(owner, timeline);
+ var t = await _timelineService.GetTimelineAsync(timelineId);
+ return await _timelineMapper.MapAsync(t, Url, User);
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline/ErrorCodes.cs b/BackEnd/Timeline/ErrorCodes.cs
index 9201979f..712ac374 100644
--- a/BackEnd/Timeline/ErrorCodes.cs
+++ b/BackEnd/Timeline/ErrorCodes.cs
@@ -79,6 +79,7 @@
public static class TimelineController
{
public const int QueryRelateNotExist = 1_104_04_01;
+ public const int MultipleTimelineWithSameName = 1_104_04_02;
}
public static class HighlightTimelineController
diff --git a/BackEnd/Timeline/Filters/CatchMultipleTimelineExceptionAttribute.cs b/BackEnd/Timeline/Filters/CatchMultipleTimelineExceptionAttribute.cs
new file mode 100644
index 00000000..db4a91bd
--- /dev/null
+++ b/BackEnd/Timeline/Filters/CatchMultipleTimelineExceptionAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Timeline.Models.Http;
+using Timeline.Services.Timeline;
+
+namespace Timeline.Filters
+{
+ public class CatchMultipleTimelineExceptionAttribute : ExceptionFilterAttribute
+ {
+ public override void OnException(ExceptionContext context)
+ {
+ if (context.Exception is MultipleTimelineException)
+ {
+ context.Result = new BadRequestObjectResult(new CommonResponse(ErrorCodes.TimelineController.MultipleTimelineWithSameName, Resource.MessageMultipleTimeline));
+ }
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline/Filters/Resource.Designer.cs b/BackEnd/Timeline/Filters/Resource.Designer.cs
index 25d0e222..d4e088cc 100644
--- a/BackEnd/Timeline/Filters/Resource.Designer.cs
+++ b/BackEnd/Timeline/Filters/Resource.Designer.cs
@@ -1,126 +1,132 @@
-//------------------------------------------------------------------------------
-// <auto-generated>
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-// </auto-generated>
-//------------------------------------------------------------------------------
-
-namespace Timeline.Filters {
- using System;
-
-
- /// <summary>
- /// A strongly-typed resource class, for looking up localized strings, etc.
- /// </summary>
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resource {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resource() {
- }
-
- /// <summary>
- /// Returns the cached ResourceManager instance used by this class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Filters.Resource", typeof(Resource).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- /// <summary>
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Content length is too big. It can&apos;t be bigger than {0}..
- /// </summary>
- internal static string MessageContentLengthTooBig {
- get {
- return ResourceManager.GetString("MessageContentLengthTooBig", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to The entity you want to create already exists. Entity type is {0}. Constraints are {1}..
- /// </summary>
- internal static string MessageEntityAlreadyExist {
- get {
- return ResourceManager.GetString("MessageEntityAlreadyExist", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to The entity you operate on does not exist. Entity type is {0}. Constraints are {1}..
- /// </summary>
- internal static string MessageEntityNotExist {
- get {
- return ResourceManager.GetString("MessageEntityNotExist", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Image is not a square..
- /// </summary>
- internal static string MessageImageBadSize {
- get {
- return ResourceManager.GetString("MessageImageBadSize", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Image decode failed..
- /// </summary>
- internal static string MessageImageDecodeFailed {
- get {
- return ResourceManager.GetString("MessageImageDecodeFailed", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Specified image format does not match the actual one ..
- /// </summary>
- internal static string MessageImageFormatUnmatch {
- get {
- return ResourceManager.GetString("MessageImageFormatUnmatch", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Unknown error happened to image..
- /// </summary>
- internal static string MessageImageUnknownError {
- get {
- return ResourceManager.GetString("MessageImageUnknownError", resourceCulture);
- }
- }
- }
-}
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Filters {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// This class was generated by MSBuild using the GenerateResource task.
+ /// To add or remove a member, edit your .resx file then rerun MSBuild.
+ /// </summary>
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Filters.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Content length is too big. It can&apos;t be bigger than {0}..
+ /// </summary>
+ internal static string MessageContentLengthTooBig {
+ get {
+ return ResourceManager.GetString("MessageContentLengthTooBig", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The entity you want to create already exists. Entity type is {0}. Constraints are {1}..
+ /// </summary>
+ internal static string MessageEntityAlreadyExist {
+ get {
+ return ResourceManager.GetString("MessageEntityAlreadyExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The entity you operate on does not exist. Entity type is {0}. Constraints are {1}..
+ /// </summary>
+ internal static string MessageEntityNotExist {
+ get {
+ return ResourceManager.GetString("MessageEntityNotExist", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image is not a square..
+ /// </summary>
+ internal static string MessageImageBadSize {
+ get {
+ return ResourceManager.GetString("MessageImageBadSize", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Image decode failed..
+ /// </summary>
+ internal static string MessageImageDecodeFailed {
+ get {
+ return ResourceManager.GetString("MessageImageDecodeFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Specified image format does not match the actual one ..
+ /// </summary>
+ internal static string MessageImageFormatUnmatch {
+ get {
+ return ResourceManager.GetString("MessageImageFormatUnmatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Unknown error happened to image..
+ /// </summary>
+ internal static string MessageImageUnknownError {
+ get {
+ return ResourceManager.GetString("MessageImageUnknownError", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Multiple timeline with the name exists. Please use new api..
+ /// </summary>
+ internal static string MessageMultipleTimeline {
+ get {
+ return ResourceManager.GetString("MessageMultipleTimeline", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Filters/Resource.resx b/BackEnd/Timeline/Filters/Resource.resx
index 15b13f97..92bca0e1 100644
--- a/BackEnd/Timeline/Filters/Resource.resx
+++ b/BackEnd/Timeline/Filters/Resource.resx
@@ -138,4 +138,7 @@
<data name="MessageImageUnknownError" xml:space="preserve">
<value>Unknown error happened to image.</value>
</data>
+ <data name="MessageMultipleTimeline" xml:space="preserve">
+ <value>Multiple timeline with the name exists. Please use new api.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/BackEnd/Timeline/Models/Validation/NameValidator.cs b/BackEnd/Timeline/Models/Validation/NameValidator.cs
index 2220de6f..c7c18738 100644
--- a/BackEnd/Timeline/Models/Validation/NameValidator.cs
+++ b/BackEnd/Timeline/Models/Validation/NameValidator.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.RegularExpressions;
namespace Timeline.Models.Validation
@@ -7,10 +8,17 @@ namespace Timeline.Models.Validation
{
private static Regex UniqueIdRegex { get; } = new Regex(@"^[a-zA-Z0-9]{32}$");
+ public List<string> DisallowedNames { get; set; } = new List<string>();
+
public const int MaxLength = 26;
protected override (bool, string) DoValidate(string value)
{
+ if (DisallowedNames.Contains(value))
+ {
+ return (false, Resource.NameDisallowed);
+ }
+
if (value.Length == 0)
{
return (false, Resource.NameCantBeEmpty);
diff --git a/BackEnd/Timeline/Models/Validation/Resource.Designer.cs b/BackEnd/Timeline/Models/Validation/Resource.Designer.cs
index 47ad4248..4f52e047 100644
--- a/BackEnd/Timeline/Models/Validation/Resource.Designer.cs
+++ b/BackEnd/Timeline/Models/Validation/Resource.Designer.cs
@@ -1,153 +1,159 @@
-//------------------------------------------------------------------------------
-// <auto-generated>
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-// </auto-generated>
-//------------------------------------------------------------------------------
-
-namespace Timeline.Models.Validation {
- using System;
-
-
- /// <summary>
- /// A strongly-typed resource class, for looking up localized strings, etc.
- /// </summary>
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resource {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resource() {
- }
-
- /// <summary>
- /// Returns the cached ResourceManager instance used by this class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Models.Validation.Resource", typeof(Resource).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- /// <summary>
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- /// </summary>
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to it can&apos;t be null.
- /// </summary>
- internal static string CantBeNull {
- get {
- return ResourceManager.GetString("CantBeNull", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can&apos;t be empty.
- /// </summary>
- internal static string NameCantBeEmpty {
- get {
- return ResourceManager.GetString("NameCantBeEmpty", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can&apos;t be longer than {0}.
- /// </summary>
- internal static string NameCantBeLongerThan {
- get {
- return ResourceManager.GetString("NameCantBeLongerThan", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can&apos;t has the same pattern of unique id.
- /// </summary>
- internal static string NameCantBeUniqueIdPattern {
- get {
- return ResourceManager.GetString("NameCantBeUniqueIdPattern", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can only consists of alphabet, number, &apos;_&apos; and &apos;-&apos; .
- /// </summary>
- internal static string NameInvalidChar {
- get {
- return ResourceManager.GetString("NameInvalidChar", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to nickname can&apos;t be longer than 25.
- /// </summary>
- internal static string NicknameTooLong {
- get {
- return ResourceManager.GetString("NicknameTooLong", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to it is not of type {0}.
- /// </summary>
- internal static string NotOfType {
- get {
- return ResourceManager.GetString("NotOfType", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Failed to create validator instance..
- /// </summary>
- internal static string ValidateWithAttributeExceptionCreateFail {
- get {
- return ResourceManager.GetString("ValidateWithAttributeExceptionCreateFail", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Given type is not a IValidator instance..
- /// </summary>
- internal static string ValidateWithAttributeExceptionNotValidator {
- get {
- return ResourceManager.GetString("ValidateWithAttributeExceptionNotValidator", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to validation passed.
- /// </summary>
- internal static string ValidationPassed {
- get {
- return ResourceManager.GetString("ValidationPassed", resourceCulture);
- }
- }
- }
-}
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Models.Validation {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// This class was generated by MSBuild using the GenerateResource task.
+ /// To add or remove a member, edit your .resx file then rerun MSBuild.
+ /// </summary>
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resource() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Models.Validation.Resource", typeof(Resource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to it can&apos;t be null.
+ /// </summary>
+ internal static string CantBeNull {
+ get {
+ return ResourceManager.GetString("CantBeNull", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to name can&apos;t be empty.
+ /// </summary>
+ internal static string NameCantBeEmpty {
+ get {
+ return ResourceManager.GetString("NameCantBeEmpty", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to name can&apos;t be longer than {0}.
+ /// </summary>
+ internal static string NameCantBeLongerThan {
+ get {
+ return ResourceManager.GetString("NameCantBeLongerThan", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to name can&apos;t has the same pattern of unique id.
+ /// </summary>
+ internal static string NameCantBeUniqueIdPattern {
+ get {
+ return ResourceManager.GetString("NameCantBeUniqueIdPattern", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to this special value is not allowed.
+ /// </summary>
+ internal static string NameDisallowed {
+ get {
+ return ResourceManager.GetString("NameDisallowed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to name can only consists of alphabet, number, &apos;_&apos; and &apos;-&apos; .
+ /// </summary>
+ internal static string NameInvalidChar {
+ get {
+ return ResourceManager.GetString("NameInvalidChar", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to nickname can&apos;t be longer than 25.
+ /// </summary>
+ internal static string NicknameTooLong {
+ get {
+ return ResourceManager.GetString("NicknameTooLong", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to it is not of type {0}.
+ /// </summary>
+ internal static string NotOfType {
+ get {
+ return ResourceManager.GetString("NotOfType", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Failed to create validator instance..
+ /// </summary>
+ internal static string ValidateWithAttributeExceptionCreateFail {
+ get {
+ return ResourceManager.GetString("ValidateWithAttributeExceptionCreateFail", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Given type is not a IValidator instance..
+ /// </summary>
+ internal static string ValidateWithAttributeExceptionNotValidator {
+ get {
+ return ResourceManager.GetString("ValidateWithAttributeExceptionNotValidator", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to validation passed.
+ /// </summary>
+ internal static string ValidationPassed {
+ get {
+ return ResourceManager.GetString("ValidationPassed", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/BackEnd/Timeline/Models/Validation/Resource.resx b/BackEnd/Timeline/Models/Validation/Resource.resx
index 68ba3810..9bec20a5 100644
--- a/BackEnd/Timeline/Models/Validation/Resource.resx
+++ b/BackEnd/Timeline/Models/Validation/Resource.resx
@@ -120,6 +120,9 @@
<data name="CantBeNull" xml:space="preserve">
<value>it can't be null</value>
</data>
+ <data name="NameDisallowed" xml:space="preserve">
+ <value>this special value is not allowed</value>
+ </data>
<data name="NameCantBeEmpty" xml:space="preserve">
<value>name can't be empty</value>
</data>
diff --git a/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs
index f1ab54e8..7cd49fd2 100644
--- a/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs
+++ b/BackEnd/Timeline/Models/Validation/TimelineNameValidator.cs
@@ -1,5 +1,6 @@
using System;
-
+using System.Collections.Generic;
+
namespace Timeline.Models.Validation
{
public class TimelineNameValidator : NameValidator
@@ -12,8 +13,8 @@ namespace Timeline.Models.Validation
{
public TimelineNameAttribute()
: base(typeof(TimelineNameValidator))
- {
-
+ {
+
}
}
}
diff --git a/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs b/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs
index 9d6ec93f..b48f99c9 100644
--- a/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs
+++ b/BackEnd/Timeline/Services/Api/BookmarkTimelineService.cs
@@ -12,10 +12,10 @@ namespace Timeline.Services.Api
public class BookmarkTimelineService : IBookmarkTimelineService
{
private readonly DatabaseContext _database;
- private readonly IBasicUserService _userService;
- private readonly IBasicTimelineService _timelineService;
+ private readonly IUserService _userService;
+ private readonly ITimelineService _timelineService;
- public BookmarkTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService)
+ public BookmarkTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService)
{
_database = database;
_userService = userService;
diff --git a/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs b/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs
index eb606ae6..837f3091 100644
--- a/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs
+++ b/BackEnd/Timeline/Services/Api/HighlightTimelineService.cs
@@ -11,11 +11,11 @@ namespace Timeline.Services.Api
public class HighlightTimelineService : IHighlightTimelineService
{
private readonly DatabaseContext _database;
- private readonly IBasicUserService _userService;
- private readonly IBasicTimelineService _timelineService;
+ private readonly IUserService _userService;
+ private readonly ITimelineService _timelineService;
private readonly IClock _clock;
- public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, IBasicTimelineService timelineService, IClock clock)
+ public HighlightTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService, IClock clock)
{
_database = database;
_userService = userService;
diff --git a/BackEnd/Timeline/Services/EntityType.cs b/BackEnd/Timeline/Services/EntityType.cs
index c379d211..5111904f 100644
--- a/BackEnd/Timeline/Services/EntityType.cs
+++ b/BackEnd/Timeline/Services/EntityType.cs
@@ -1,8 +1,9 @@
-using System.Reflection;
+using System;
+using System.Reflection;
namespace Timeline.Services
{
- public class EntityType
+ public class EntityType : IEquatable<EntityType>
{
public EntityType(string name)
{
@@ -29,6 +30,24 @@ namespace Timeline.Services
if (field is not null) return (int)field.GetRawConstantValue()!;
return ErrorCodes.Conflict.Default;
}
- }
+ }
+
+ public bool Equals(EntityType? other)
+ {
+ if (other is null)
+ return false;
+
+ return other.Name.Equals(other.Name);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as EntityType);
+ }
+
+ public override int GetHashCode()
+ {
+ return Name.GetHashCode();
+ }
}
}
diff --git a/BackEnd/Timeline/Services/Timeline/BasicTimelineService.cs b/BackEnd/Timeline/Services/Timeline/BasicTimelineService.cs
deleted file mode 100644
index 360f6c63..00000000
--- a/BackEnd/Timeline/Services/Timeline/BasicTimelineService.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-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.User;
-
-namespace Timeline.Services.Timeline
-{
- public class BasicTimelineService : IBasicTimelineService
- {
- private readonly ILogger<BasicTimelineService> _logger;
-
- private readonly DatabaseContext _database;
-
- private readonly IBasicUserService _basicUserService;
- private readonly IClock _clock;
-
- private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator();
-
- public BasicTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IBasicUserService basicUserService, IClock clock)
- {
- _logger = loggerFactory.CreateLogger<BasicTimelineService>();
- _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>()
- };
- }
-
- protected static EntityNotExistException CreateTimelineNotExistException(string name, Exception? inner = null)
- {
- return new EntityNotExistException(EntityTypes.Timeline, new Dictionary<string, object>
- {
- ["name"] = name
- }, null, inner);
- }
-
- protected static EntityNotExistException CreateTimelineNotExistException(long id)
- {
- return new EntityNotExistException(EntityTypes.Timeline, new Dictionary<string, object>
- {
- ["id"] = id
- });
- }
-
- protected void CheckGeneralTimelineName(string timelineName, string? paramName)
- {
- if (!_generalTimelineNameValidator.Validate(timelineName, out var message))
- throw new ArgumentException(string.Format(Resource.ExceptionGeneralTimelineNameBadFormat, message), paramName);
- }
-
- public async Task<bool> CheckTimelineExistenceAsync(long id)
- {
- return await _database.Timelines.AnyAsync(t => t.Id == id);
- }
-
- public async Task<long> GetTimelineIdByNameAsync(string timelineName)
- {
- if (timelineName == null)
- throw new ArgumentNullException(nameof(timelineName));
-
- CheckGeneralTimelineName(timelineName, nameof(timelineName));
-
- var name = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
-
- if (isPersonal)
- {
- var username = name;
- long userId;
- try
- {
- userId = await _basicUserService.GetUserIdByUsernameAsync(username);
- }
- catch (EntityNotExistException e)
- {
- throw CreateTimelineNotExistException(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();
-
- _logger.LogInformation(Resource.LogPersonalTimelineAutoCreate, username);
-
- return newTimelineEntity.Id;
- }
- }
- else
- {
- var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync();
-
- if (timelineEntity == null)
- {
- throw CreateTimelineNotExistException(timelineName);
- }
- else
- {
- return timelineEntity.Id;
- }
- }
- }
- }
-}
diff --git a/BackEnd/Timeline/Services/Timeline/BasicTimelineServiceExtensions.cs b/BackEnd/Timeline/Services/Timeline/BasicTimelineServiceExtensions.cs
deleted file mode 100644
index 4075c9c0..00000000
--- a/BackEnd/Timeline/Services/Timeline/BasicTimelineServiceExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Timeline.Services.Timeline
-{
- public static class BasicTimelineServiceExtensions
- {
- public static async Task ThrowIfTimelineNotExist(this IBasicTimelineService service, long timelineId)
- {
- if (!await service.CheckTimelineExistenceAsync(timelineId))
- {
- throw new EntityNotExistException(EntityTypes.Timeline,
- new Dictionary<string, object> { ["id"] = timelineId });
- }
- }
- }
-}
diff --git a/BackEnd/Timeline/Services/Timeline/IBasicTimelineService.cs b/BackEnd/Timeline/Services/Timeline/IBasicTimelineService.cs
deleted file mode 100644
index b6f1e6de..00000000
--- a/BackEnd/Timeline/Services/Timeline/IBasicTimelineService.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Timeline.Services.Timeline
-{
- /// <summary>
- /// This service provide some basic timeline functions, which should be used internally for other services.
- /// </summary>
- public interface IBasicTimelineService
- {
- /// <summary>
- /// Check whether a timeline with given id exists without getting full info.
- /// </summary>
- /// <param name="id">The timeline id.</param>
- /// <returns>True if exist. Otherwise false.</returns>
- Task<bool> CheckTimelineExistenceAsync(long id);
-
- /// <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="EntityNotExistException">
- /// Thrown when timeline with name <paramref name="timelineName"/> does not exist.
- /// </exception>
- Task<long> GetTimelineIdByNameAsync(string timelineName);
- }
-}
diff --git a/BackEnd/Timeline/Services/Timeline/ITimelineService.cs b/BackEnd/Timeline/Services/Timeline/ITimelineService.cs
index c5bd5abf..847caf0d 100644
--- a/BackEnd/Timeline/Services/Timeline/ITimelineService.cs
+++ b/BackEnd/Timeline/Services/Timeline/ITimelineService.cs
@@ -9,8 +9,48 @@ namespace Timeline.Services.Timeline
/// <summary>
/// This define the interface of both personal timeline and ordinary timeline.
/// </summary>
- public interface ITimelineService : IBasicTimelineService
- {
+ public interface ITimelineService
+ {
+ /// <summary>
+ /// Check whether a timeline with given id exists without getting full info.
+ /// </summary>
+ /// <param name="id">The timeline id.</param>
+ /// <returns>True if exist. Otherwise false.</returns>
+ Task<bool> CheckTimelineExistenceAsync(long id);
+
+ /// <summary>
+ /// Get the timeline id by name. Deprecated now because different users can have timeline with the same name now.
+ /// </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="EntityNotExistException">Thrown when timeline with name <paramref name="timelineName"/> does not exist.</exception>
+ /// <exception cref="MultipleTimelineException">Thrown when multiple timelines have that name.</exception>
+ Task<long> GetTimelineIdByNameAsync(string timelineName);
+
+ /// <summary>
+ /// Get timeline id by owner id and timeline name.
+ /// </summary>
+ /// <param name="ownerId">The timeline owner id.</param>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <returns>A task contains timeline id.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="timelineName"/> is not a valid name.</exception>
+ Task<long> GetTimelineIdAsync(long ownerId, string timelineName);
+
+ /// <summary>
+ /// Get timeline id by owner username and timeline name.
+ /// </summary>
+ /// <param name="ownerUsername">The timeline owner id.</param>
+ /// <param name="timelineName">The timeline name.</param>
+ /// <returns>A task contains timeline id.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="ownerUsername"/> is null or <paramref name="timelineName"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown when <paramref name="ownerUsername"/> is not a valid username or <paramref name="timelineName"/> is not a valid timeline name.</exception>
+ /// <exception cref="EntityNotExistException">Thrown when user with given username does not exist.</exception>
+ /// <exception cref="EntityNotExistException">Thrown when timeline with given name does not exist.</exception>
+ Task<long> GetTimelineIdAsync(string ownerUsername, string timelineName);
+
/// <summary>
/// Get the timeline info.
/// </summary>
@@ -102,14 +142,14 @@ namespace Timeline.Services.Timeline
/// <summary>
/// Create a timeline.
/// </summary>
- /// <param name="timelineName">The name of the timeline.</param>
/// <param name="ownerId">The id of owner of the timeline.</param>
+ /// <param name="timelineName">The name of the timeline.</param>
/// <returns>The info of the new timeline.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="timelineName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when timeline name is invalid.</exception>
/// <exception cref="EntityAlreadyExistException">Thrown when the timeline already exists.</exception>
/// <exception cref="EntityNotExistException">Thrown when the owner user does not exist.</exception>
- Task<TimelineEntity> CreateTimelineAsync(string timelineName, long ownerId);
+ Task<TimelineEntity> CreateTimelineAsync(long ownerId, string timelineName);
/// <summary>
/// Delete a timeline.
diff --git a/BackEnd/Timeline/Services/Timeline/MultipleTimelineException.cs b/BackEnd/Timeline/Services/Timeline/MultipleTimelineException.cs
new file mode 100644
index 00000000..f7f80a31
--- /dev/null
+++ b/BackEnd/Timeline/Services/Timeline/MultipleTimelineException.cs
@@ -0,0 +1,44 @@
+using System;
+namespace Timeline.Services.Timeline
+{
+ /// <summary>
+ /// Thrown when call <see cref="ITimelineService.GetTimelineIdByNameAsync(string)"/> and multiple timelines have that same name.
+ /// </summary>
+ [Serializable]
+ public class MultipleTimelineException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:MultipleTimelineException"/> class
+ /// </summary>
+ public MultipleTimelineException()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:MultipleTimelineException"/> class
+ /// </summary>
+ /// <param name="message">A <see cref="T:System.String"/> that describes the exception. </param>
+ public MultipleTimelineException(string message) : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:MultipleTimelineException"/> class
+ /// </summary>
+ /// <param name="message">A <see cref="T:System.String"/> that describes the exception. </param>
+ /// <param name="inner">The exception that is the cause of the current exception. </param>
+ public MultipleTimelineException(string message, System.Exception inner) : base(message, inner)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:MultipleTimelineException"/> class
+ /// </summary>
+ /// <param name="context">The contextual information about the source or destination.</param>
+ /// <param name="info">The object that holds the serialized object data.</param>
+ protected MultipleTimelineException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
+
diff --git a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
index ae034767..e6297d2c 100644
--- a/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
+++ b/BackEnd/Timeline/Services/Timeline/TimelinePostService.cs
@@ -20,15 +20,15 @@ namespace Timeline.Services.Timeline
{
private readonly ILogger<TimelinePostService> _logger;
private readonly DatabaseContext _database;
- private readonly IBasicTimelineService _basicTimelineService;
- private readonly IBasicUserService _basicUserService;
+ private readonly ITimelineService _basicTimelineService;
+ private readonly IUserService _basicUserService;
private readonly IDataManager _dataManager;
private readonly IImageService _imageValidator;
private readonly IClock _clock;
private readonly ColorValidator _colorValidator = new ColorValidator();
private readonly ColorValidator _colorValidatorAllowEmptyAndDefault = new ColorValidator() { PermitEmpty = true, PermitDefault = true };
- public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageService imageValidator, IClock clock)
+ public TimelinePostService(ILogger<TimelinePostService> logger, DatabaseContext database, ITimelineService basicTimelineService, IUserService basicUserService, IDataManager dataManager, IImageService imageValidator, IClock clock)
{
_logger = logger;
_database = database;
diff --git a/BackEnd/Timeline/Services/Timeline/TimelineService.cs b/BackEnd/Timeline/Services/Timeline/TimelineService.cs
index 6f22ff05..cdea39fa 100644
--- a/BackEnd/Timeline/Services/Timeline/TimelineService.cs
+++ b/BackEnd/Timeline/Services/Timeline/TimelineService.cs
@@ -12,33 +12,34 @@ using Timeline.Services.User;
namespace Timeline.Services.Timeline
{
- public class TimelineService : BasicTimelineService, ITimelineService
+ public class TimelineService : ITimelineService
{
private readonly ILogger<TimelineService> _logger;
private readonly DatabaseContext _database;
- private readonly IBasicUserService _userService;
+ private readonly IUserService _userService;
private readonly IClock _clock;
+ private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator();
private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
private readonly ColorValidator _colorValidator = new ColorValidator() { PermitDefault = true, PermitEmpty = true };
- public TimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IBasicUserService userService, IClock clock)
- : base(loggerFactory, database, userService, clock)
+ public TimelineService(ILogger<TimelineService> logger, DatabaseContext database, IUserService userService, IClock clock)
{
- _logger = loggerFactory.CreateLogger<TimelineService>();
+ _logger = logger;
_database = database;
_userService = userService;
_clock = clock;
}
- private static EntityAlreadyExistException CreateTimelineConflictException(string name)
+ private static EntityAlreadyExistException CreateTimelineConflictException(long ownerId, string timelineName)
{
return new EntityAlreadyExistException(EntityTypes.Timeline, new Dictionary<string, object>
{
- ["name"] = name
+ [nameof(ownerId)] = ownerId,
+ [nameof(timelineName)] = timelineName
});
}
@@ -48,8 +49,120 @@ namespace Timeline.Services.Timeline
{
throw new ArgumentException(string.Format(Resource.ExceptionTimelineNameBadFormat, message), paramName);
}
+ }
+
+ 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>()
+ };
+ }
+
+ protected static EntityNotExistException CreateTimelineNotExistException(string name, Exception? inner = null)
+ {
+ return new EntityNotExistException(EntityTypes.Timeline, new Dictionary<string, object>
+ {
+ ["name"] = name
+ }, null, inner);
+ }
+
+ protected static EntityNotExistException CreateTimelineNotExistException(long id)
+ {
+ return new EntityNotExistException(EntityTypes.Timeline, new Dictionary<string, object>
+ {
+ ["id"] = id
+ });
+ }
+
+ protected static EntityNotExistException CreateTimelineNotExistException(long ownerId, string timelineName)
+ {
+ return new EntityNotExistException(EntityTypes.Timeline, new Dictionary<string, object>
+ {
+ [nameof(ownerId)] = ownerId,
+ [nameof(timelineName)] = timelineName
+ });
+ }
+
+ protected void CheckGeneralTimelineName(string timelineName, string? paramName)
+ {
+ if (!_generalTimelineNameValidator.Validate(timelineName, out var message))
+ throw new ArgumentException(string.Format(Resource.ExceptionGeneralTimelineNameBadFormat, message), paramName);
+ }
+
+ public async Task<bool> CheckTimelineExistenceAsync(long id)
+ {
+ return await _database.Timelines.AnyAsync(t => t.Id == id);
+ }
+
+ public async Task<long> GetTimelineIdByNameAsync(string timelineName)
+ {
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
+
+ CheckGeneralTimelineName(timelineName, nameof(timelineName));
+
+ var name = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
+
+ if (isPersonal)
+ {
+ var username = name;
+ long userId;
+ try
+ {
+ userId = await _userService.GetUserIdByUsernameAsync(username);
+ }
+ catch (EntityNotExistException e)
+ {
+ throw CreateTimelineNotExistException(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();
+
+ _logger.LogInformation(Resource.LogPersonalTimelineAutoCreate, username);
+
+ return newTimelineEntity.Id;
+ }
+ }
+ else
+ {
+ var timelineEntities = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).ToListAsync();
+
+ if (timelineEntities.Count == 0)
+ {
+ throw CreateTimelineNotExistException(timelineName);
+ }
+ else if (timelineEntities.Count == 1)
+ {
+ return timelineEntities[0].Id;
+ }
+ else
+ {
+ throw new MultipleTimelineException(String.Format("Multiple timelines have name '{}'.", timelineName));
+ }
+ }
}
+
public async Task<TimelineEntity> GetTimelineAsync(long id)
{
var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
@@ -87,10 +200,10 @@ namespace Timeline.Services.Timeline
if (newProperties.Name is not null)
{
- var conflict = await _database.Timelines.AnyAsync(t => t.Name == newProperties.Name);
+ var conflict = await _database.Timelines.AnyAsync(t => t.OwnerId == entity.OwnerId && t.Name == newProperties.Name);
if (conflict)
- throw CreateTimelineConflictException(newProperties.Name);
+ throw CreateTimelineConflictException(entity.OwnerId, newProperties.Name);
entity.Name = newProperties.Name;
@@ -268,23 +381,27 @@ namespace Timeline.Services.Timeline
return entities;
}
- public async Task<TimelineEntity> CreateTimelineAsync(string name, long owner)
+ public async Task<TimelineEntity> CreateTimelineAsync(long ownerId, string timelineName)
{
- if (name == null)
- throw new ArgumentNullException(nameof(name));
+ if (timelineName == null)
+ throw new ArgumentNullException(nameof(timelineName));
- CheckTimelineName(name, nameof(name));
+ CheckTimelineName(timelineName, nameof(timelineName));
+ if (timelineName == "self")
+ {
+ throw new ArgumentException("Timeline name can't be 'self'.");
+ }
- var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
+ var conflict = await _database.Timelines.AnyAsync(t => t.OwnerId == ownerId && t.Name == timelineName);
if (conflict)
- throw CreateTimelineConflictException(name);
+ throw CreateTimelineConflictException(ownerId, timelineName);
- var entity = CreateNewTimelineEntity(name, owner);
+ var entity = CreateNewTimelineEntity(timelineName, ownerId);
_database.Timelines.Add(entity);
await _database.SaveChangesAsync();
- _logger.LogInformation(Resource.LogTimelineCreate, name, entity.Id);
+ _logger.LogInformation(Resource.LogTimelineCreate, timelineName, entity.Id);
return entity;
}
@@ -299,6 +416,27 @@ namespace Timeline.Services.Timeline
_database.Timelines.Remove(entity);
await _database.SaveChangesAsync();
_logger.LogWarning(Resource.LogTimelineDelete, id);
- }
+ }
+
+ public async Task<long> GetTimelineIdAsync(long ownerId, string timelineName)
+ {
+ if (timelineName is null)
+ throw new ArgumentNullException(nameof(timelineName));
+ CheckTimelineName(timelineName, nameof(timelineName));
+
+ string? tn = timelineName == "self" ? null : timelineName;
+
+ var entity = await _database.Timelines.Where(t => t.OwnerId == ownerId && t.Name == tn).SingleOrDefaultAsync();
+ if (entity is null)
+ throw CreateTimelineNotExistException(ownerId, timelineName);
+
+ return entity.Id;
+ }
+
+ public async Task<long> GetTimelineIdAsync(string ownerUsername, string timelineName)
+ {
+ var ownerId = await _userService.GetUserIdByUsernameAsync(ownerUsername);
+ return await GetTimelineIdAsync(ownerId, timelineName);
+ }
}
}
diff --git a/BackEnd/Timeline/Services/Timeline/TimelineServiceExtensions.cs b/BackEnd/Timeline/Services/Timeline/TimelineServiceExtensions.cs
index 7b745168..48f63a5f 100644
--- a/BackEnd/Timeline/Services/Timeline/TimelineServiceExtensions.cs
+++ b/BackEnd/Timeline/Services/Timeline/TimelineServiceExtensions.cs
@@ -5,7 +5,16 @@ using Timeline.Entities;
namespace Timeline.Services.Timeline
{
public static class TimelineServiceExtensions
- {
+ {
+ public static async Task ThrowIfTimelineNotExist(this ITimelineService service, long timelineId)
+ {
+ if (!await service.CheckTimelineExistenceAsync(timelineId))
+ {
+ throw new EntityNotExistException(EntityTypes.Timeline,
+ new Dictionary<string, object> { ["id"] = timelineId });
+ }
+ }
+
public static async Task<List<TimelineEntity>> GetTimelineList(this ITimelineService service, IEnumerable<long> ids)
{
var timelines = new List<TimelineEntity>();
diff --git a/BackEnd/Timeline/Services/Timeline/TimelineServicesServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/Timeline/TimelineServicesServiceCollectionExtensions.cs
index 97b313cd..41c8e246 100644
--- a/BackEnd/Timeline/Services/Timeline/TimelineServicesServiceCollectionExtensions.cs
+++ b/BackEnd/Timeline/Services/Timeline/TimelineServicesServiceCollectionExtensions.cs
@@ -7,7 +7,6 @@ namespace Timeline.Services.Timeline
{
public static IServiceCollection AddTimelineServices(this IServiceCollection services)
{
- services.TryAddScoped<IBasicTimelineService, BasicTimelineService>();
services.TryAddScoped<ITimelineService, TimelineService>();
services.TryAddScoped<ITimelinePostService, TimelinePostService>();
services.TryAddScoped<MarkdownProcessor>();
diff --git a/BackEnd/Timeline/Services/User/Avatar/UserAvatarService.cs b/BackEnd/Timeline/Services/User/Avatar/UserAvatarService.cs
index 92979a68..8b1a69a0 100644
--- a/BackEnd/Timeline/Services/User/Avatar/UserAvatarService.cs
+++ b/BackEnd/Timeline/Services/User/Avatar/UserAvatarService.cs
@@ -15,7 +15,7 @@ namespace Timeline.Services.User.Avatar
{
private readonly ILogger<UserAvatarService> _logger;
private readonly DatabaseContext _database;
- private readonly IBasicUserService _basicUserService;
+ private readonly IUserService _userService;
private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
private readonly IImageService _imageService;
private readonly IDataManager _dataManager;
@@ -24,7 +24,7 @@ namespace Timeline.Services.User.Avatar
public UserAvatarService(
ILogger<UserAvatarService> logger,
DatabaseContext database,
- IBasicUserService basicUserService,
+ IUserService basicUserService,
IDefaultUserAvatarProvider defaultUserAvatarProvider,
IImageService imageValidator,
IDataManager dataManager,
@@ -32,7 +32,7 @@ namespace Timeline.Services.User.Avatar
{
_logger = logger;
_database = database;
- _basicUserService = basicUserService;
+ _userService = basicUserService;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_imageService = imageValidator;
_dataManager = dataManager;
@@ -41,7 +41,7 @@ namespace Timeline.Services.User.Avatar
public async Task<ICacheableDataDigest> GetAvatarDigestAsync(long userId)
{
- var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTimeAsync(userId);
+ var usernameChangeTime = await _userService.GetUsernameLastModifiedTimeAsync(userId);
var entity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.DataTag, a.LastModified }).SingleOrDefaultAsync();
@@ -63,7 +63,7 @@ namespace Timeline.Services.User.Avatar
public async Task<ByteData> GetAvatarAsync(long userId)
{
- await _basicUserService.ThrowIfUserNotExist(userId);
+ await _userService.ThrowIfUserNotExist(userId);
var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
@@ -91,7 +91,7 @@ namespace Timeline.Services.User.Avatar
await _imageService.ValidateAsync(avatar.Data, avatar.ContentType, true);
- await _basicUserService.ThrowIfUserNotExist(userId);
+ await _userService.ThrowIfUserNotExist(userId);
var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
@@ -133,7 +133,7 @@ namespace Timeline.Services.User.Avatar
public async Task DeleteAvatarAsync(long userId)
{
- await _basicUserService.ThrowIfUserNotExist(userId);
+ await _userService.ThrowIfUserNotExist(userId);
var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
diff --git a/BackEnd/Timeline/Services/User/BasicUserService.cs b/BackEnd/Timeline/Services/User/BasicUserService.cs
deleted file mode 100644
index 0ee8dabd..00000000
--- a/BackEnd/Timeline/Services/User/BasicUserService.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Models.Validation;
-
-namespace Timeline.Services.User
-{
- public class BasicUserService : IBasicUserService
- {
- private readonly DatabaseContext _database;
-
- private readonly UsernameValidator _usernameValidator = new UsernameValidator();
-
- public BasicUserService(DatabaseContext database)
- {
- _database = database;
- }
-
- protected static EntityNotExistException CreateUserNotExistException(string username)
- {
- return new EntityNotExistException(EntityTypes.User,
- new Dictionary<string, object> { ["username"] = username });
- }
-
- protected static EntityNotExistException CreateUserNotExistException(long id)
- {
- return new EntityNotExistException(EntityTypes.User,
- new Dictionary<string, object> { ["id"] = id });
- }
-
- public async Task<bool> CheckUserExistenceAsync(long id)
- {
- return await _database.Users.AnyAsync(u => u.Id == id);
- }
-
- public async Task<long> GetUserIdByUsernameAsync(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 CreateUserNotExistException(username);
-
- return entity.Id;
- }
-
- public async Task<DateTime> GetUsernameLastModifiedTimeAsync(long userId)
- {
- var entity = await _database.Users.Where(u => u.Id == userId).Select(u => new { u.UsernameChangeTime }).SingleOrDefaultAsync();
-
- if (entity is null)
- throw CreateUserNotExistException(userId);
-
- return entity.UsernameChangeTime;
- }
- }
-}
diff --git a/BackEnd/Timeline/Services/User/CreateTokenResult.cs b/BackEnd/Timeline/Services/User/CreateTokenResult.cs
deleted file mode 100644
index b71a9e9e..00000000
--- a/BackEnd/Timeline/Services/User/CreateTokenResult.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using Timeline.Entities;
-
-namespace Timeline.Services.User
-{
- public class CreateTokenResult
- {
- public string Token { get; set; } = default!;
- public UserEntity User { get; set; } = default!;
- }
-}
-
diff --git a/BackEnd/Timeline/Services/User/IBasicUserService.cs b/BackEnd/Timeline/Services/User/IBasicUserService.cs
deleted file mode 100644
index 0ae3fdff..00000000
--- a/BackEnd/Timeline/Services/User/IBasicUserService.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Timeline.Services.User
-{
- /// <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> CheckUserExistenceAsync(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="EntityNotExistException">Thrown when the user with given username does not exist.</exception>
- Task<long> GetUserIdByUsernameAsync(string username);
-
- /// <summary>
- /// Get the username modified time of a user.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <returns>The time.</returns>
- /// <exception cref="EntityNotExistException">Thrown when user does not exist.</exception>
- Task<DateTime> GetUsernameLastModifiedTimeAsync(long userId);
- }
-}
diff --git a/BackEnd/Timeline/Services/User/IUserService.cs b/BackEnd/Timeline/Services/User/IUserService.cs
index 745bd524..6ea9a4d2 100644
--- a/BackEnd/Timeline/Services/User/IUserService.cs
+++ b/BackEnd/Timeline/Services/User/IUserService.cs
@@ -5,8 +5,33 @@ using Timeline.Entities;
namespace Timeline.Services.User
{
- public interface IUserService : IBasicUserService
- {
+ public interface IUserService
+ {
+ /// <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> CheckUserExistenceAsync(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="EntityNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<long> GetUserIdByUsernameAsync(string username);
+
+ /// <summary>
+ /// Get the username modified time of a user.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <returns>The time.</returns>
+ /// <exception cref="EntityNotExistException">Thrown when user does not exist.</exception>
+ Task<DateTime> GetUsernameLastModifiedTimeAsync(long userId);
+
/// <summary>
/// Try to get a user by id.
/// </summary>
diff --git a/BackEnd/Timeline/Services/User/UserPermissionService.cs b/BackEnd/Timeline/Services/User/UserPermissionService.cs
index f6f11c61..69c23b84 100644
--- a/BackEnd/Timeline/Services/User/UserPermissionService.cs
+++ b/BackEnd/Timeline/Services/User/UserPermissionService.cs
@@ -8,19 +8,19 @@ namespace Timeline.Services.User
public class UserPermissionService : IUserPermissionService
{
private readonly DatabaseContext _database;
- private readonly IBasicUserService _basicUserService;
+ private readonly IUserService _userService;
- public UserPermissionService(DatabaseContext database, IBasicUserService basicUserService)
+ public UserPermissionService(DatabaseContext database, IUserService userService)
{
_database = database;
- _basicUserService = basicUserService;
+ _userService = userService;
}
private async Task CheckUserExistence(long userId, bool checkUserExistence)
{
if (checkUserExistence)
{
- await _basicUserService.ThrowIfUserNotExist(userId);
+ await _userService.ThrowIfUserNotExist(userId);
}
}
diff --git a/BackEnd/Timeline/Services/User/UserService.cs b/BackEnd/Timeline/Services/User/UserService.cs
index 1ad74bec..d5ee9a2f 100644
--- a/BackEnd/Timeline/Services/User/UserService.cs
+++ b/BackEnd/Timeline/Services/User/UserService.cs
@@ -11,12 +11,12 @@ using Timeline.Services.Token;
namespace Timeline.Services.User
{
- public class UserService : BasicUserService, IUserService
+ public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
private readonly IClock _clock;
- private readonly DatabaseContext _databaseContext;
+ private readonly DatabaseContext _database;
private readonly IPasswordService _passwordService;
@@ -25,10 +25,10 @@ namespace Timeline.Services.User
private readonly UsernameValidator _usernameValidator = new UsernameValidator();
private readonly NicknameValidator _nicknameValidator = new NicknameValidator();
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserTokenService userTokenService, IClock clock) : base(databaseContext)
+ public UserService(ILogger<UserService> logger, DatabaseContext database, IPasswordService passwordService, IUserTokenService userTokenService, IClock clock)
{
_logger = logger;
- _databaseContext = databaseContext;
+ _database = database;
_passwordService = passwordService;
_userTokenService = userTokenService;
_clock = clock;
@@ -58,15 +58,56 @@ namespace Timeline.Services.User
}
}
+ private static EntityNotExistException CreateUserNotExistException(string username)
+ {
+ throw new EntityNotExistException(EntityTypes.User, new Dictionary<string, object> { ["username"] = username });
+ }
+
+ private static EntityNotExistException CreateUserNotExistException(long userId)
+ {
+ throw new EntityNotExistException(EntityTypes.User, new Dictionary<string, object> { ["id"] = userId });
+ }
+
private static EntityAlreadyExistException CreateUsernameConflictException(string username)
{
throw new EntityAlreadyExistException(EntityTypes.User,
new Dictionary<string, object> { ["username"] = username });
+ }
+
+ public async Task<bool> CheckUserExistenceAsync(long id)
+ {
+ return await _database.Users.AnyAsync(u => u.Id == id);
+ }
+
+ public async Task<long> GetUserIdByUsernameAsync(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 CreateUserNotExistException(username);
+
+ return entity.Id;
+ }
+
+ public async Task<DateTime> GetUsernameLastModifiedTimeAsync(long userId)
+ {
+ var entity = await _database.Users.Where(u => u.Id == userId).Select(u => new { u.UsernameChangeTime }).SingleOrDefaultAsync();
+
+ if (entity is null)
+ throw CreateUserNotExistException(userId);
+
+ return entity.UsernameChangeTime;
}
public async Task<UserEntity> GetUserAsync(long id)
{
- var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ var user = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
if (user is null)
throw CreateUserNotExistException(id);
@@ -76,7 +117,7 @@ namespace Timeline.Services.User
public async Task<List<UserEntity>> GetUsersAsync()
{
- return await _databaseContext.Users.ToListAsync();
+ return await _database.Users.ToListAsync();
}
public async Task<UserEntity> CreateUserAsync(CreateUserParams param)
@@ -92,7 +133,7 @@ namespace Timeline.Services.User
if (param.Nickname is not null)
CheckNicknameFormat(param.Nickname, nameof(param));
- var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == param.Username);
+ var conflict = await _database.Users.AnyAsync(u => u.Username == param.Username);
if (conflict)
throw CreateUsernameConflictException(param.Username);
@@ -103,8 +144,8 @@ namespace Timeline.Services.User
Nickname = param.Nickname,
Version = 1
};
- _databaseContext.Users.Add(newEntity);
- await _databaseContext.SaveChangesAsync();
+ _database.Users.Add(newEntity);
+ await _database.SaveChangesAsync();
_logger.LogInformation(Resource.LogUserCreated, param.Username, newEntity.Id);
return newEntity;
@@ -124,7 +165,7 @@ namespace Timeline.Services.User
CheckNicknameFormat(param.Nickname, nameof(param));
}
- var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw CreateUserNotExistException(id);
@@ -136,7 +177,7 @@ namespace Timeline.Services.User
var username = param.Username;
if (username is not null && username != entity.Username)
{
- var conflict = await _databaseContext.Users.AnyAsync(u => u.Username == username);
+ var conflict = await _database.Users.AnyAsync(u => u.Username == username);
if (conflict)
throw CreateUsernameConflictException(username);
@@ -164,7 +205,7 @@ namespace Timeline.Services.User
entity.LastModified = now;
}
- await _databaseContext.SaveChangesAsync();
+ await _database.SaveChangesAsync();
_logger.LogInformation(Resource.LogUserModified, entity.Username, id);
if (password is not null)
@@ -185,7 +226,7 @@ namespace Timeline.Services.User
CheckUsernameFormat(username, nameof(username));
CheckPasswordFormat(password, nameof(password));
- var entity = await _databaseContext.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync();
+ var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync();
if (entity is null)
{
@@ -211,7 +252,7 @@ namespace Timeline.Services.User
CheckPasswordFormat(oldPassword, nameof(oldPassword));
CheckPasswordFormat(newPassword, nameof(newPassword));
- var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+ var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
if (entity is null)
throw CreateUserNotExistException(id);
@@ -221,7 +262,7 @@ namespace Timeline.Services.User
entity.Password = _passwordService.HashPassword(newPassword);
entity.Version += 1;
- await _databaseContext.SaveChangesAsync();
+ await _database.SaveChangesAsync();
_logger.LogInformation(Resource.LogChangePassowrd, entity.Username, id);
await _userTokenService.RevokeAllTokenByUserIdAsync(id);
diff --git a/BackEnd/Timeline/Services/User/BasicUserServiceExtensions.cs b/BackEnd/Timeline/Services/User/UserServiceExtensions.cs
index 8a05f452..82b55fba 100644
--- a/BackEnd/Timeline/Services/User/BasicUserServiceExtensions.cs
+++ b/BackEnd/Timeline/Services/User/UserServiceExtensions.cs
@@ -3,9 +3,9 @@ using System.Threading.Tasks;
namespace Timeline.Services.User
{
- public static class BasicUserServiceExtensions
+ public static class UserServiceExtensions
{
- public static async Task ThrowIfUserNotExist(this IBasicUserService service, long userId)
+ public static async Task ThrowIfUserNotExist(this IUserService service, long userId)
{
if (!await service.CheckUserExistenceAsync(userId))
{
diff --git a/BackEnd/Timeline/Services/User/UserServicesServiceCollectionExtensions.cs b/BackEnd/Timeline/Services/User/UserServicesServiceCollectionExtensions.cs
index a5a743df..76d101a0 100644
--- a/BackEnd/Timeline/Services/User/UserServicesServiceCollectionExtensions.cs
+++ b/BackEnd/Timeline/Services/User/UserServicesServiceCollectionExtensions.cs
@@ -9,7 +9,6 @@ namespace Timeline.Services.User
public static IServiceCollection AddUserServices(this IServiceCollection services)
{
services.TryAddTransient<IPasswordService, PasswordService>();
- services.TryAddScoped<IBasicUserService, BasicUserService>();
services.TryAddScoped<IUserService, UserService>();
services.TryAddScoped<IUserDeleteService, UserDeleteService>();
services.TryAddScoped<IUserPermissionService, UserPermissionService>();
diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj
index 0bf11e31..26e562e8 100644
--- a/BackEnd/Timeline/Timeline.csproj
+++ b/BackEnd/Timeline/Timeline.csproj
@@ -36,7 +36,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
- <PackageReference Include="Markdig" Version="0.28.0" />
+ <PackageReference Include="Markdig" Version="0.28.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="6.0.3" />
diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json
index f3bbe4b3..258d3e8d 100644
--- a/BackEnd/Timeline/packages.lock.json
+++ b/BackEnd/Timeline/packages.lock.json
@@ -23,9 +23,9 @@
},
"Markdig": {
"type": "Direct",
- "requested": "[0.28.0, )",
- "resolved": "0.28.0",
- "contentHash": "gT6Sm14OaKQDwus5wFoI/v+Zxrgd2r70pkfq6dC1mIZaK1rvdEHtfnf3nD/Q/clrPFcvBIxZag79NXFjMkxhmA=="
+ "requested": "[0.28.1, )",
+ "resolved": "0.28.1",
+ "contentHash": "70CneXw2N/1t7v6OfZJqMKLPRB1YWTPddEIcHT/P6IL6X1zsXELIu/DHVt96kr83PIVLznMuXoFK6b9N9KTODg=="
},
"Microsoft.AspNetCore.SpaServices.Extensions": {
"type": "Direct",
diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 11126690..b4016499 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -52,7 +52,7 @@
"@types/node": "^17.0.23",
"@types/react": "^17.0.43",
"@types/react-color": "^3.0.6",
- "@types/react-dom": "^17.0.14",
+ "@types/react-dom": "^18.0.0",
"@types/react-responsive": "^8.0.5",
"@types/react-transition-group": "^4.4.4",
"@types/remarkable": "^2.0.3",