Merge pull request #17924 from abpframework/EngincanV/cms-comments

CMS Kit: Prevent duplicate comment records
pull/17928/head
Gizem Mutu Kurt 2 years ago committed by GitHub
commit 32a6a19148
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;
}

@ -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."
}
}

@ -25,9 +25,11 @@ public class Comment : AggregateRoot<Guid>, IHasCreationTime, IMustHaveCreator,
public virtual string Url { get; set; }
public virtual string IdempotencyToken { get; set; }
protected Comment()
{
}
internal Comment(

@ -44,4 +44,6 @@ public interface ICommentRepository : IBasicRepository<Comment, Guid>
Comment comment,
CancellationToken cancellationToken = default
);
Task<bool> ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default);
}

@ -141,6 +141,11 @@ public class EfCoreCommentRepository : EfCoreRepository<ICmsKitDbContext, Commen
await DeleteAsync(comment, cancellationToken: GetCancellationToken(cancellationToken));
}
public virtual async Task<bool> ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync()).AnyAsync(x => x.IdempotencyToken == idempotencyToken, GetCancellationToken(cancellationToken));
}
protected virtual async Task<IQueryable<CommentWithAuthorQueryResultItem>> GetListQueryAsync(
string filter = null,
string entityType = null,

@ -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 });

@ -159,6 +159,12 @@ public class MongoCommentRepository : MongoDbRepository<ICmsKitMongoDbContext, C
}
}
public virtual async Task<bool> ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))
.AnyAsync(x => x.IdempotencyToken == idempotencyToken, GetCancellationToken(cancellationToken));
}
protected virtual async Task<IQueryable<Comment>> GetListQueryAsync(
string filter = null,
string entityType = null,

@ -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,7 @@ public class CreateCommentInput: ExtensibleObject
public int CaptchaAnswer { get; set; }
public string Url { get; set; }
[Required]
public string IdempotencyToken { get; set; }
}

@ -25,4 +25,7 @@ public class CreateCommentWithParametersInput
public int CaptchaAnswer { get; set; }
public string Url { get; set; }
[Required]
public string IdempotencyToken { get; set; }
}

@ -63,14 +63,15 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli
public virtual async Task<CommentDto> 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<CmsUser, CmsUserDto>(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"]);
}
}

@ -31,6 +31,7 @@
data-reply-id="@(repliedCommentId?.ToString() ?? "")"
style="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? "" : "display:none")">
<form class="cms-comment-form">
<input hidden value="@(Guid.NewGuid().ToString("N"))" name="idempotencyToken" />
<input hidden value="@(repliedCommentId?.ToString() ?? "")" name="repliedCommentId" />
<div class="row">
<div class="col">

@ -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();
}
});
});

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
@ -48,7 +49,8 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase
new CreateCommentInput
{
RepliedCommentId = null,
Text = "newComment"
Text = "newComment",
IdempotencyToken = Guid.NewGuid().ToString("N")
}
);
@ -75,7 +77,8 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase
new CreateCommentInput
{
RepliedCommentId = null,
Text = text
Text = text,
IdempotencyToken = Guid.NewGuid().ToString("N")
}
);
}
@ -95,6 +98,25 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase
{
RepliedCommentId = null,
Text = text, //not allowed URL
IdempotencyToken = Guid.NewGuid().ToString("N")
}
));
}
[Fact]
public async Task CreateAsync_ShouldThrowUserFriendlyException_If_IdempotencyToken_Not_Unique()
{
_currentUser.Id.Returns(_cmsKitTestData.User2Id);
await Should.ThrowAsync<UserFriendlyException>(async () =>
await _commentAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateCommentInput
{
RepliedCommentId = null,
Text = "<text>",
IdempotencyToken = _cmsKitTestData.IdempotencyToken_1
}
));
}

@ -194,7 +194,7 @@ public class CmsKitDataSeedContributor : IDataSeedContributor, ITransientDepende
"comment",
null,
_cmsKitTestData.User1Id
));
){ IdempotencyToken = _cmsKitTestData.IdempotencyToken_1 });
await _commentRepository.InsertAsync(new Comment(_guidGenerator.Create(),
_cmsKitTestData.EntityType1,

@ -131,4 +131,6 @@ public class CmsKitTestData : ISingletonDependency
public string PollName { get; } = "Poll";
public string WidgetName { get; } = "CmsPollByCode";
public string IdempotencyToken_1 { get; } = Guid.NewGuid().ToString("N");
}

Loading…
Cancel
Save