diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs index aa253c9593..59afde6a45 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs @@ -12,4 +12,6 @@ public static class CommentConsts public static int MaxTextLength { get; set; } = 512; public static int MaxUrlLength { get; set; } = 512; + + public static int MaxIdempotencyTokenLength { get; set; } = 32; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json index 40639fc46a..e879b7e339 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json @@ -225,6 +225,7 @@ "RemoveCoverImage": "Remove cover image", "CssClass": "CSS Class", "TagsHelpText": "Tags should be comma-separated (e.g.: tag1, tag2, tag3)", - "ThisPartOfContentCouldntBeLoaded": "This part of content couldn't be loaded." + "ThisPartOfContentCouldntBeLoaded": "This part of content couldn't be loaded.", + "DuplicateCommentAttemptMessage": "Duplicate comment post attempt detected. Your comment has already been submitted." } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs index 7a620b8655..7a09b18b63 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs @@ -25,9 +25,11 @@ public class Comment : AggregateRoot, IHasCreationTime, IMustHaveCreator, public virtual string Url { get; set; } + public virtual string IdempotencyToken { get; set; } + protected Comment() { - + } internal Comment( diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs index 30541c2230..b054f11037 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs @@ -44,4 +44,6 @@ public interface ICommentRepository : IBasicRepository Comment comment, CancellationToken cancellationToken = default ); + + Task ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default); } diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs index 3102bcc40b..2f626a5cd7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs @@ -141,6 +141,11 @@ public class EfCoreCommentRepository : EfCoreRepository ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default) + { + return await (await GetDbSetAsync()).AnyAsync(x => x.IdempotencyToken == idempotencyToken, GetCancellationToken(cancellationToken)); + } + protected virtual async Task> GetListQueryAsync( string filter = null, string entityType = null, diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs index a6a92cced4..b81eb4f31d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs @@ -80,6 +80,7 @@ public static class CmsKitDbContextModelCreatingExtensions b.Property(x => x.Text).IsRequired().HasMaxLength(CommentConsts.MaxTextLength); b.Property(x => x.RepliedCommentId); b.Property(x => x.Url).HasMaxLength(CommentConsts.MaxUrlLength); + b.Property(x => x.IdempotencyToken).HasMaxLength(CommentConsts.MaxIdempotencyTokenLength); b.HasIndex(x => new { x.TenantId, x.EntityType, x.EntityId }); b.HasIndex(x => new { x.TenantId, x.RepliedCommentId }); diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs index 089b86cc4f..698c533b4e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs @@ -159,6 +159,12 @@ public class MongoCommentRepository : MongoDbRepository ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default) + { + return await (await GetMongoQueryableAsync(cancellationToken)) + .AnyAsync(x => x.IdempotencyToken == idempotencyToken, GetCancellationToken(cancellationToken)); + } + protected virtual async Task> GetListQueryAsync( string filter = null, string entityType = null, diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentInput.cs index 316ffe2737..cc5ee3efec 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentInput.cs @@ -7,7 +7,7 @@ using Volo.CmsKit.Comments; namespace Volo.CmsKit.Public.Comments; [Serializable] -public class CreateCommentInput: ExtensibleObject +public class CreateCommentInput : ExtensibleObject { [Required] [DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.MaxTextLength))] @@ -20,4 +20,6 @@ public class CreateCommentInput: ExtensibleObject public int CaptchaAnswer { get; set; } public string Url { get; set; } + + public string IdempotencyToken { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs index 53f993cf2b..ec7aad1398 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs @@ -25,4 +25,6 @@ public class CreateCommentWithParametersInput public int CaptchaAnswer { get; set; } public string Url { get; set; } + + public string IdempotencyToken { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs index c1b5e736ec..cda077bde7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs @@ -63,14 +63,15 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli public virtual async Task CreateAsync(string entityType, string entityId, CreateCommentInput input) { CheckExternalUrls(entityType, input.Text); - - var user = await CmsUserLookupService.GetByIdAsync(CurrentUser.GetId()); if (input.RepliedCommentId.HasValue) { await CommentRepository.GetAsync(input.RepliedCommentId.Value); } + await CheckIdempotencyTokenUniquenessAsync(input.IdempotencyToken); + + var user = await CmsUserLookupService.GetByIdAsync(CurrentUser.GetId()); var comment = await CommentRepository.InsertAsync( await CommentManager.CreateAsync( user, @@ -192,4 +193,14 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli { return ObjectMapper.Map(comments.Single(c => c.Comment.Id == commentId).Author); } + + private async Task CheckIdempotencyTokenUniquenessAsync(string idempotencyToken) + { + if(!await CommentRepository.ExistsAsync(idempotencyToken)) + { + return; + } + + throw new UserFriendlyException(L["DuplicateCommentAttemptMessage"]); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index 8829df73c5..c1171d8ba6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -31,6 +31,7 @@ data-reply-id="@(repliedCommentId?.ToString() ?? "")" style="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? "" : "display:none")">
+
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js index 75f3288982..0d4616fe51 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js @@ -107,10 +107,15 @@ function registerUpdateOfNewComment($container) { $container.find('.cms-comment-update-form').each(function () { - let $form = $(this); + var $form = $(this); + $form.submit(function (e) { e.preventDefault(); + + abp.ui.setBusy($form.find("button[type='submit']")); + let formAsObject = $form.serializeFormToObject(); + $.ajax({ type: 'POST', url: '/CmsKitPublicComments/Update/' + formAsObject.id, @@ -124,9 +129,11 @@ }), success: function () { widgetManager.refresh($widget); + abp.ui.clearBusy(); }, error: function (data) { abp.message.error(data.responseJSON.error.message); + abp.ui.clearBusy(); } }); }); @@ -135,10 +142,14 @@ function registerSubmissionOfNewComment($container) { $container.find('.cms-comment-form').each(function () { - let $form = $(this); + var $form = $(this); + $form.submit(function (e) { e.preventDefault(); - let formAsObject = $form.serializeFormToObject(); + + abp.ui.setBusy("button[type='submit']"); + + var formAsObject = $form.serializeFormToObject(); if (formAsObject.repliedCommentId == '') { formAsObject.repliedCommentId = null; @@ -146,6 +157,7 @@ if (formAsObject.commentText == '') { abp.message.error(l("CommentTextRequired")); + abp.ui.clearBusy(); return; } @@ -161,13 +173,16 @@ text: formAsObject.commentText, url: window.location.href, captchaToken: formAsObject.captchaId, - captchaAnswer: formAsObject.input?.captcha + captchaAnswer: formAsObject.input?.captcha, + idempotencyToken: formAsObject.idempotencyToken }), success: function () { widgetManager.refresh($widget); + abp.ui.clearBusy(); }, error: function (data) { abp.message.error(data.responseJSON.error.message); + abp.ui.clearBusy(); } }); });