diff options
Diffstat (limited to 'BackEnd/Timeline')
33 files changed, 756 insertions, 650 deletions
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'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'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't be null.
- /// </summary>
- internal static string CantBeNull {
- get {
- return ResourceManager.GetString("CantBeNull", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can't be empty.
- /// </summary>
- internal static string NameCantBeEmpty {
- get {
- return ResourceManager.GetString("NameCantBeEmpty", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to name can'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'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, '_' and '-' .
- /// </summary>
- internal static string NameInvalidChar {
- get {
- return ResourceManager.GetString("NameInvalidChar", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to nickname can'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't be null. + /// </summary> + internal static string CantBeNull { + get { + return ResourceManager.GetString("CantBeNull", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to name can't be empty. + /// </summary> + internal static string NameCantBeEmpty { + get { + return ResourceManager.GetString("NameCantBeEmpty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to name can'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'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, '_' and '-' . + /// </summary> + internal static string NameInvalidChar { + get { + return ResourceManager.GetString("NameInvalidChar", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to nickname can'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", |