From 35753f08a0eb042c68e9719f46a30075d51c3906 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 17 Jun 2020 22:41:17 +0200 Subject: [PATCH] Feature/notifo (#529) * Notifo integration --- .../Extensions/DateTimeFluidExtension.cs | 1 - .../Comments/CommentsCommandMiddleware.cs | 25 +- .../Contents/ContentHistoryEventsCreator.cs | 13 +- .../Contents/Text/TextIndexingProcess.cs | 6 +- .../History/HistoryService.cs | 16 +- .../History/NotifoOptions.cs | 26 ++ .../History/NotifoService.cs | 279 ++++++++++++++++++ .../Rules/RuleDequeuerGrain.cs | 2 +- .../Rules/RuleEnqueuer.cs | 2 +- .../Schemas/SchemaHistoryEventsCreator.cs | 2 +- .../Squidex.Domain.Apps.Entities.csproj | 2 + .../DefaultUserResolver.cs | 35 +++ ...NoopUserEvents.cs => IUserEventHandler.cs} | 12 +- .../src/Squidex.Domain.Users/IUserEvents.cs | 2 + .../src/Squidex.Domain.Users/UserEvents.cs | 49 +++ .../Identity/SquidexClaimTypes.cs | 2 + .../src/Squidex.Shared/Users/IUserResolver.cs | 4 + .../Statistics/Models/CallsUsagePerDateDto.cs | 2 +- .../Frontend/Middlewares/IndexExtensions.cs | 8 + .../Frontend/Middlewares/NotifoMiddleware.cs | 61 ++++ backend/src/Squidex/Areas/Frontend/Startup.cs | 1 + .../Config/IdentityServerServices.cs | 3 +- .../Controllers/Account/AccountController.cs | 4 +- .../Controllers/Profile/ProfileController.cs | 24 +- .../Squidex/Config/Domain/HistoryServices.cs | 10 +- .../Config/Domain/SubscriptionServices.cs | 2 +- backend/src/Squidex/Startup.cs | 2 +- .../Assets/FileTagAssetMetadataSourceTests.cs | 4 +- .../Assets/ImageAssetMetadataSourceTests.cs | 2 +- .../Assets/MongoDb/AssetsQueryFixture.cs | 2 +- .../CommentsCommandMiddlewareTests.cs | 9 +- .../Contents/Queries/ResolveAssetsTests.cs | 2 +- .../DefaultUserResolverTests.cs | 64 +++- .../EventConsumersHealthCheckTests.cs | 2 +- .../BackgroundUsageTrackerTests.cs | 2 +- .../content/content-history-page.component.ts | 2 +- .../pages/content/content-page.component.html | 2 + .../contents/contents-page.component.html | 2 + .../contributors-page.component.html | 2 + .../pages/plans/plans-page.component.html | 2 + .../angular/routers/router-2-state.spec.ts | 2 +- .../angular/routers/router-2-state.ts | 2 +- .../shared/components/notifo.component.html | 1 + .../shared/components/notifo.component.scss | 26 ++ .../app/shared/components/notifo.component.ts | 98 ++++++ frontend/app/shared/declarations.ts | 1 + frontend/app/shared/module.ts | 4 +- frontend/app/shared/services/auth.service.ts | 4 + frontend/app/shared/state/apps.state.ts | 4 + frontend/app/shared/state/contents.state.ts | 21 +- .../app/shared/state/contributors.state.ts | 4 + frontend/app/shared/state/plans.state.ts | 4 + .../notifications-menu.component.html | 29 +- .../notifications-menu.component.scss | 8 - .../internal/notifications-menu.component.ts | 83 +----- 55 files changed, 775 insertions(+), 208 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs rename backend/src/Squidex.Domain.Users/{NoopUserEvents.cs => IUserEventHandler.cs} (65%) create mode 100644 backend/src/Squidex.Domain.Users/UserEvents.cs create mode 100644 backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs create mode 100644 frontend/app/shared/components/notifo.component.html create mode 100644 frontend/app/shared/components/notifo.component.scss create mode 100644 frontend/app/shared/components/notifo.component.ts diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs index 3b3582d22..701e12c73 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs @@ -10,7 +10,6 @@ using Fluid; using Fluid.Values; using NodaTime; using NodaTime.Text; -using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Templates.Extensions { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs index 6ce58e3ec..860159c27 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs @@ -15,8 +15,6 @@ using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Comments @@ -43,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Comments { if (commentsCommand is CreateComment createComment && !IsMention(createComment)) { - await ReplicateCommandAsync(context, createComment); + await MentionUsersAsync(createComment); } await ExecuteCommandAsync(context, commentsCommand); @@ -52,27 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Comments await next(context); } - private async Task ReplicateCommandAsync(CommandContext context, CommentTextCommand command) - { - await MentionUsersAsync(command); - - if (command.Mentions != null) - { - foreach (var userId in command.Mentions) - { - var notificationCommand = SimpleMapper.Map(command, new CreateComment()); - - notificationCommand.AppId = null!; - notificationCommand.Mentions = null; - notificationCommand.CommentsId = userId; - notificationCommand.ExpectedVersion = EtagVersion.Any; - notificationCommand.IsMention = true; - - context.CommandBus.PublishAsync(notificationCommand).Forget(); - } - } - } - private async Task ExecuteCommandAsync(CommandContext context, CommentsCommand commentsCommand) { var grain = grainFactory.GetGrain(commentsCommand.CommentsId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs index b2e9c4904..4dc41f3a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -41,15 +41,13 @@ namespace Squidex.Domain.Apps.Entities.Contents "changed status of {[Schema]} content to {[Status]}."); AddEventMessage( - "scheduled to change status of {[Schema]} content to {[Status]}."); + "scheduled to change status of {[Schemra]} content to {[Status]}."); } protected override Task CreateEventCoreAsync(Envelope @event) { var channel = $"contents.{@event.Headers.AggregateId()}"; - var result = ForEvent(@event.Payload, channel); - if (@event.Payload is SchemaEvent schemaEvent) { if (schemaEvent.SchemaId == null) @@ -57,7 +55,14 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.FromResult(null); } - result = result.Param("Schema", schemaEvent.SchemaId.Name); + channel = $"schemas.{schemaEvent.SchemaId.Id}.{channel}"; + } + + var result = ForEvent(@event.Payload, channel); + + if (@event.Payload is SchemaEvent schemaEvent2) + { + result = result.Param("Schema", schemaEvent2.SchemaId.Name); } if (@event.Payload is ContentStatusChanged contentStatusChanged) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs index 2fe8cd4f7..ca4c64fc4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text DocId = state.DocIdCurrent, ServeAll = true, ServePublished = false, - Texts = data.ToTexts(), + Texts = data.ToTexts() }); await textIndexerState.SetAsync(state); @@ -245,7 +245,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text }, new DeleteIndexEntry { - DocId = state.DocIdNew, + DocId = state.DocIdNew }); state.DocIdNew = null; @@ -267,7 +267,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text }, new DeleteIndexEntry { - DocId = state.DocIdNew ?? NotFound, + DocId = state.DocIdNew ?? NotFound }); await textIndexerState.RemoveAsync(state.ContentId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index b79fe4590..81048ab6d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -21,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.History private readonly Dictionary texts = new Dictionary(); private readonly List creators; private readonly IHistoryEventRepository repository; + private readonly NotifoService notifo; public string Name { @@ -32,10 +33,11 @@ namespace Squidex.Domain.Apps.Entities.History get { return ".*"; } } - public HistoryService(IHistoryEventRepository repository, IEnumerable creators) + public HistoryService(IHistoryEventRepository repository, IEnumerable creators, NotifoService notifo) { Guard.NotNull(repository, nameof(repository)); Guard.NotNull(creators, nameof(creators)); + Guard.NotNull(notifo, nameof(notifo)); this.creators = creators.ToList(); @@ -48,6 +50,8 @@ namespace Squidex.Domain.Apps.Entities.History } this.repository = repository; + + this.notifo = notifo; } public bool Handles(StoredEvent @event) @@ -62,16 +66,20 @@ namespace Squidex.Domain.Apps.Entities.History public async Task On(Envelope @event) { + await notifo.HandleEventAsync(@event); + foreach (var creator in creators) { var historyEvent = await creator.CreateEventAsync(@event); if (historyEvent != null) { - var appEvent = (AppEvent)@event.Payload; + var appEvent = @event.To(); + + await notifo.HandleHistoryEventAsync(appEvent, historyEvent); - historyEvent.Actor = appEvent.Actor; - historyEvent.AppId = appEvent.AppId.Id; + historyEvent.Actor = appEvent.Payload.Actor; + historyEvent.AppId = appEvent.Payload.AppId.Id; historyEvent.Created = @event.Headers.Timestamp(); historyEvent.Version = @event.Headers.EventStreamNumber(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs new file mode 100644 index 000000000..07c4a555f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoOptions.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class NotifoOptions + { + public string AppId { get; set; } + + public string ApiKey { get; set; } + + public string ApiUrl { get; set; } = "https://app.notifo.io"; + + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(ApiKey) && + !string.IsNullOrWhiteSpace(ApiUrl) && + !string.IsNullOrWhiteSpace(AppId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs new file mode 100644 index 000000000..d6e052455 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -0,0 +1,279 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using NodaTime; +using Notifo.SDK; +using Notifo.Services; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Comments; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Users; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using static Notifo.Services.Notifications; + +namespace Squidex.Domain.Apps.Entities.History +{ + public class NotifoService : IInitializable, IUserEventHandler + { + private static readonly Duration MaxAge = Duration.FromHours(12); + private readonly NotifoOptions options; + private readonly IUrlGenerator urlGenerator; + private readonly IUserResolver userResolver; + private readonly IClock clock; + private NotificationsClient? client; + + public NotifoService(IOptions options, IUrlGenerator urlGenerator, IUserResolver userResolver, IClock clock) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(urlGenerator, nameof(urlGenerator)); + Guard.NotNull(userResolver, nameof(userResolver)); + Guard.NotNull(clock, nameof(clock)); + + this.options = options.Value; + + this.urlGenerator = urlGenerator; + this.userResolver = userResolver; + + this.clock = clock; + } + + public Task InitializeAsync(CancellationToken ct = default) + { + if (options.IsConfigured()) + { + var builder = + NotificationsClientBuilder.Create() + .SetApiKey(options.ApiKey); + + if (!string.IsNullOrWhiteSpace(options.ApiUrl)) + { + builder = builder.SetApiUrl(options.ApiUrl); + } + + client = builder.Build(); + } + + return Task.CompletedTask; + } + + public void OnUserUpdated(IUser user) + { + UpsertUserAsync(user).Forget(); + } + + private async Task UpsertUserAsync(IUser user) + { + if (client == null) + { + return; + } + + var settings = new NotificationSettingsDto(); + + settings.Channels[Providers.WebPush] = new NotificationSettingDto + { + Send = true, + DelayInSeconds = null + }; + + settings.Channels[Providers.Email] = new NotificationSettingDto + { + Send = true, + DelayInSeconds = 5 * 60 + }; + + var userRequest = new UpsertUserRequest + { + AppId = options.AppId, + EmailAddress = user.Email, + FullName = user.DisplayName(), + PreferredLanguage = "en", + PreferredTimezone = null, + RequiresWhitelistedTopic = true, + Settings = settings, + UserId = user.Id + }; + + var response = await client.UpsertUserAsync(userRequest); + + await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.NotifoKey, response.User.ApiKey); + } + + public async Task HandleEventAsync(Envelope @event) + { + Guard.NotNull(@event, nameof(@event)); + + if (client == null) + { + return; + } + + switch (@event.Payload) + { + case CommentCreated comment: + { + if (IsTooOld(@event.Headers)) + { + return; + } + + if (comment.Mentions == null || comment.Mentions.Length == 0) + { + break; + } + + using (var stream = client.PublishMany()) + { + foreach (var userId in comment.Mentions) + { + var publishRequest = new PublishRequest + { + AppId = options.AppId + }; + + publishRequest.Topic = $"users/{userId}"; + + publishRequest.Properties["SquidexApp"] = comment.AppId.Name; + publishRequest.Preformatted = new NotificationFormattingDto(); + publishRequest.Preformatted.Subject["en"] = comment.Text; + + if (comment.Url?.IsAbsoluteUri == true) + { + publishRequest.Preformatted.LinkUrl["en"] = comment.Url.ToString(); + } + + SetUser(comment, publishRequest); + + await stream.RequestStream.WriteAsync(publishRequest); + } + + await stream.RequestStream.CompleteAsync(); + await stream.ResponseAsync; + } + + break; + } + + case AppContributorAssigned contributorAssigned: + { + var user = await userResolver.FindByIdAsync(contributorAssigned.ContributorId); + + if (user != null) + { + await UpsertUserAsync(user); + } + + var request = BuildAllowedTopicRequest(contributorAssigned, contributorAssigned.ContributorId); + + await client.AddAllowedTopicAsync(request); + + break; + } + + case AppContributorRemoved contributorRemoved: + { + var request = BuildAllowedTopicRequest(contributorRemoved, contributorRemoved.ContributorId); + + await client.RemoveAllowedTopicAsync(request); + + break; + } + } + } + + private AllowedTopicRequest BuildAllowedTopicRequest(AppEvent @event, string contributorId) + { + var topicRequest = new AllowedTopicRequest + { + AppId = options.AppId + }; + + topicRequest.UserId = contributorId; + topicRequest.TopicPrefix = GetAppPrefix(@event); + + return topicRequest; + } + + public async Task HandleHistoryEventAsync(Envelope @event, HistoryEvent historyEvent) + { + if (client == null) + { + return; + } + + if (IsTooOld(@event.Headers)) + { + return; + } + + var appEvent = @event.Payload; + + var publishRequest = new PublishRequest + { + AppId = options.AppId + }; + + foreach (var (key, value) in historyEvent.Parameters) + { + publishRequest.Properties.Add(key, value); + } + + publishRequest.Properties["SquidexApp"] = appEvent.AppId.Name; + + if (appEvent is ContentEvent c && !(appEvent is ContentDeleted)) + { + var url = urlGenerator.ContentUI(c.AppId, c.SchemaId, c.ContentId); + + publishRequest.Properties["SquidexUrl"] = url; + } + + publishRequest.TemplateCode = historyEvent.EventType; + + SetUser(appEvent, publishRequest); + SetTopic(appEvent, publishRequest, historyEvent); + + await client.PublishAsync(publishRequest); + } + + private bool IsTooOld(EnvelopeHeaders headers) + { + var now = clock.GetCurrentInstant(); + + return now - headers.Timestamp() > MaxAge; + } + + private static void SetUser(AppEvent appEvent, PublishRequest publishRequest) + { + if (appEvent.Actor.IsSubject) + { + publishRequest.CreatorId = appEvent.Actor.Identifier; + } + } + + private static void SetTopic(AppEvent appEvent, PublishRequest publishRequest, HistoryEvent @event) + { + var topicPrefix = GetAppPrefix(appEvent); + var topicSuffix = @event.Channel.Replace('.', '/').Trim(); + + publishRequest.Topic = $"{topicPrefix}/{topicSuffix}"; + } + + private static string GetAppPrefix(AppEvent appEvent) + { + return $"apps/{appEvent.AppId.Id}"; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs index a44892d92..dcbe3a8e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -112,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Rules ExecutionResult = response.Status, Finished = now, JobNext = jobDelay, - JobResult = ComputeJobResult(response.Status, jobDelay), + JobResult = ComputeJobResult(response.Status, jobDelay) }; await ruleEventRepository.UpdateAsync(@event.Job, update); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index a0d579636..d55045c6b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -50,9 +50,9 @@ namespace Squidex.Domain.Apps.Entities.Rules this.appProvider = appProvider; this.cache = cache; - this.localCache = localCache; this.ruleEventRepository = ruleEventRepository; this.ruleService = ruleService; + this.localCache = localCache; } public bool Handles(StoredEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs index 2807562d1..3fb4ae135 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas if (@event.Payload is SchemaEvent schemaEvent) { - var channel = $"schemas.{schemaEvent.SchemaId.Name}"; + var channel = $"schemas.{schemaEvent.SchemaId.Id}"; result = ForEvent(@event.Payload, channel).Param("Name", schemaEvent.SchemaId.Name); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 0551635a4..e8dc8cf01 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -12,6 +12,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs index 12ba8e55d..6dcc88e57 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -65,6 +66,28 @@ namespace Squidex.Domain.Users } } + public async Task SetClaimAsync(string id, string type, string value) + { + Guard.NotNullOrEmpty(id, nameof(id)); + Guard.NotNullOrEmpty(type, nameof(type)); + Guard.NotNullOrEmpty(value, nameof(value)); + + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var values = new UserValues + { + CustomClaims = new List + { + new Claim(type, value) + } + }; + + await userManager.UpdateAsync(id, values); + } + } + public async Task FindByIdAsync(string id) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -98,6 +121,18 @@ namespace Squidex.Domain.Users } } + public async Task> QueryAllAsync() + { + using (var scope = serviceProvider.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var result = await userManager.QueryByEmailAsync(null); + + return result.OfType().ToList(); + } + } + public async Task> QueryByEmailAsync(string email) { Guard.NotNullOrEmpty(email, nameof(email)); diff --git a/backend/src/Squidex.Domain.Users/NoopUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEventHandler.cs similarity index 65% rename from backend/src/Squidex.Domain.Users/NoopUserEvents.cs rename to backend/src/Squidex.Domain.Users/IUserEventHandler.cs index 9fb142938..0bf9f8a48 100644 --- a/backend/src/Squidex.Domain.Users/NoopUserEvents.cs +++ b/backend/src/Squidex.Domain.Users/IUserEventHandler.cs @@ -1,7 +1,7 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== @@ -9,13 +9,17 @@ using Squidex.Shared.Users; namespace Squidex.Domain.Users { - public sealed class NoopUserEvents : IUserEvents + public interface IUserEventHandler { - public void OnConsentGiven(IUser user) + void OnUserRegistered(IUser user) { } - public void OnUserRegistered(IUser user) + void OnUserUpdated(IUser user) + { + } + + void OnConsentGiven(IUser user) { } } diff --git a/backend/src/Squidex.Domain.Users/IUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEvents.cs index e92dc1a6a..8383b7770 100644 --- a/backend/src/Squidex.Domain.Users/IUserEvents.cs +++ b/backend/src/Squidex.Domain.Users/IUserEvents.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Users { void OnUserRegistered(IUser user); + void OnUserUpdated(IUser user); + void OnConsentGiven(IUser user); } } diff --git a/backend/src/Squidex.Domain.Users/UserEvents.cs b/backend/src/Squidex.Domain.Users/UserEvents.cs new file mode 100644 index 000000000..4bf14785a --- /dev/null +++ b/backend/src/Squidex.Domain.Users/UserEvents.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class UserEvents : IUserEvents + { + private readonly IEnumerable userEventHandlers; + + public UserEvents(IEnumerable userEventHandlers) + { + Guard.NotNull(userEventHandlers, nameof(userEventHandlers)); + + this.userEventHandlers = userEventHandlers; + } + + public void OnUserRegistered(IUser user) + { + foreach (var handler in userEventHandlers) + { + handler.OnUserRegistered(user); + } + } + + public void OnUserUpdated(IUser user) + { + foreach (var handler in userEventHandlers) + { + handler.OnUserUpdated(user); + } + } + + public void OnConsentGiven(IUser user) + { + foreach (var handler in userEventHandlers) + { + handler.OnConsentGiven(user); + } + } + } +} diff --git a/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs index 92cc3989a..c2594c52e 100644 --- a/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs +++ b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs @@ -13,6 +13,8 @@ namespace Squidex.Shared.Identity public static readonly string PictureUrl = "urn:squidex:picture"; + public static readonly string NotifoKey = "urn:squidex:notifo"; + public static readonly string Consent = "urn:squidex:consent"; public static readonly string ConsentForEmails = "urn:squidex:consent:emails"; diff --git a/backend/src/Squidex.Shared/Users/IUserResolver.cs b/backend/src/Squidex.Shared/Users/IUserResolver.cs index 4de82aa2f..487996d28 100644 --- a/backend/src/Squidex.Shared/Users/IUserResolver.cs +++ b/backend/src/Squidex.Shared/Users/IUserResolver.cs @@ -18,8 +18,12 @@ namespace Squidex.Shared.Users Task FindByIdAsync(string idOrEmail); + Task SetClaimAsync(string id, string type, string value); + Task> QueryByEmailAsync(string email); + Task> QueryAllAsync(); + Task> QueryManyAsync(string[] ids); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs index 1de6c8a05..b338b5e8a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/Models/CallsUsagePerDateDto.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics.Models Date = DateTime.SpecifyKind(stats.Date, DateTimeKind.Utc), TotalBytes = stats.TotalBytes, TotalCalls = stats.TotalCalls, - AverageElapsedMs = stats.AverageElapsedMs, + AverageElapsedMs = stats.AverageElapsedMs }; return result; diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index a5ae28058..de6d78aaf 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.UI; +using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure.Json; using Squidex.Web; @@ -52,6 +53,13 @@ namespace Squidex.Areas.Frontend.Middlewares uiOptions.More["info"] = values.ToString(); } + var notifo = httpContext.RequestServices.GetService>(); + + if (notifo.Value.IsConfigured()) + { + uiOptions.More["notifoApi"] = notifo.Value.ApiUrl; + } + var jsonSerializer = httpContext.RequestServices.GetRequiredService(); var jsonOptions = jsonSerializer.Serialize(uiOptions); diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs new file mode 100644 index 000000000..72cd287a0 --- /dev/null +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Squidex.Domain.Apps.Entities.History; + +namespace Squidex.Areas.Frontend.Middlewares +{ + public class NotifoMiddleware + { + private readonly RequestDelegate next; + private readonly string? workerUrl; + + public NotifoMiddleware(RequestDelegate next, IOptions options) + { + this.next = next; + + workerUrl = GetUrl(options.Value); + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.Equals("/notifo-sw.js") && workerUrl != null) + { + context.Response.Headers[HeaderNames.ContentType] = "text/javascript"; + + var script = $"importScripts('{workerUrl}')"; + + await context.Response.WriteAsync(script); + } + else + { + await next(context); + } + } + + private static string? GetUrl(NotifoOptions options) + { + if (!options.IsConfigured()) + { + return null; + } + + if (options.ApiUrl.Contains("localhost:5002")) + { + return "https://localhost:3002/notifo-sdk-worker.js"; + } + else + { + return $"{options.ApiUrl}/build/notifo-sdk-worker.js"; + } + } + } +} diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs index 9df94bfe1..2d8edf0ac 100644 --- a/backend/src/Squidex/Areas/Frontend/Startup.cs +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -25,6 +25,7 @@ namespace Squidex.Areas.Frontend var environment = app.ApplicationServices.GetRequiredService(); app.UseMiddleware(); + app.UseMiddleware(); app.Use((context, next) => { diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index 478ef3a3a..3d357a883 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -96,7 +96,8 @@ namespace Squidex.Areas.IdentityServer.Config new[] { SquidexClaimTypes.DisplayName, - SquidexClaimTypes.PictureUrl + SquidexClaimTypes.PictureUrl, + SquidexClaimTypes.NotifoKey }); } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index d31a09668..4812207e0 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -360,7 +360,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); } - private Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) + private async Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) { var update = new UserValues { @@ -382,7 +382,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account update.Permissions = new PermissionSet(Permissions.Admin); } - return MakeIdentityOperation(() => userManager.SyncClaims(user.Identity, update)); + return await MakeIdentityOperation(() => userManager.SyncClaims(user.Identity, update)); } private IActionResult RedirectToLogoutUrl(LogoutRequest context) diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 3bb135cdd..c98e2ac18 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -33,6 +33,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile private readonly SignInManager signInManager; private readonly UserManager userManager; private readonly IUserPictureStore userPictureStore; + private readonly IUserEvents userEvents; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly MyIdentityOptions identityOptions; @@ -40,6 +41,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile SignInManager signInManager, UserManager userManager, IUserPictureStore userPictureStore, + IUserEvents userEvents, IAssetThumbnailGenerator assetThumbnailGenerator, IOptions identityOptions) { @@ -47,6 +49,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile this.identityOptions = identityOptions.Value; this.userManager = userManager; this.userPictureStore = userPictureStore; + this.userEvents = userEvents; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -84,7 +87,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/update/")] public Task UpdateProfile(ChangeProfileModel model) { - return MakeChangeAsync(u => userManager.UpdateSafeAsync(u, model.ToValues()), + return MakeChangeAsync(u => UpdateAsync(u, model.ToValues()), "Account updated successfully.", model); } @@ -92,7 +95,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/properties/")] public Task UpdateProperties(ChangePropertiesModel model) { - return MakeChangeAsync(u => userManager.UpdateSafeAsync(u, model.ToValues()), + return MakeChangeAsync(u => UpdateAsync(u, model.ToValues()), "Account updated successfully.", model); } @@ -143,6 +146,23 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile return await userManager.AddLoginAsync(user, externalLogin); } + private async Task UpdateAsync(IdentityUser user, UserValues values) + { + var result = await userManager.UpdateSafeAsync(user, values); + + if (result.Succeeded) + { + var resolved = await userManager.ResolveUserAsync(user); + + if (resolved != null) + { + userEvents.OnUserUpdated(resolved); + } + } + + return result; + } + private async Task UpdatePictureAsync(List file, IdentityUser user) { if (file.Count != 1) diff --git a/backend/src/Squidex/Config/Domain/HistoryServices.cs b/backend/src/Squidex/Config/Domain/HistoryServices.cs index 3582407f5..74bf8951d 100644 --- a/backend/src/Squidex/Config/Domain/HistoryServices.cs +++ b/backend/src/Squidex/Config/Domain/HistoryServices.cs @@ -5,16 +5,24 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Entities.History; +using Squidex.Domain.Users; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Config.Domain { public static class HistoryServices { - public static void AddSquidexHistory(this IServiceCollection services) + public static void AddSquidexHistory(this IServiceCollection services, IConfiguration config) { + services.Configure( + config.GetSection("notifo")); + + services.AddSingletonAs() + .AsSelf().As(); + services.AddSingletonAs() .As().As(); } diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs index 5f4a0717e..37ef1e815 100644 --- a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -30,7 +30,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() + services.AddSingletonAs() .AsOptional(); } } diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 74d5acc4b..afa9b1502 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -54,7 +54,7 @@ namespace Squidex services.AddSquidexEventPublisher(config); services.AddSquidexEventSourcing(config); services.AddSquidexHealthChecks(config); - services.AddSquidexHistory(); + services.AddSquidexHistory(config); services.AddSquidexIdentity(config); services.AddSquidexIdentityServer(); services.AddSquidexInfrastructure(config); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index f9a6bb8d1..30cc6050f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { ["videoWidth"] = JsonValue.Create(128), ["videoHeight"] = JsonValue.Create(55), - ["duration"] = JsonValue.Create("00:10:12"), + ["duration"] = JsonValue.Create("00:10:12") }, Type = AssetType.Video }; @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { Metadata = new AssetMetadata { - ["duration"] = JsonValue.Create("00:10:12"), + ["duration"] = JsonValue.Create("00:10:12") }, Type = AssetType.Audio }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 9e0fbc648..152bced7c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Assets Metadata = new AssetMetadata() { ["pixelWidth"] = JsonValue.Create(128), - ["pixelHeight"] = JsonValue.Create(55), + ["pixelHeight"] = JsonValue.Create(55) }, Type = AssetType.Image }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index 8ed123d08..a976c8b1a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb { ["value"] = JsonValue.Create(tag) }, - Slug = fileName, + Slug = fileName }; await ExecuteBatchAsync(asset); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsCommandMiddlewareTests.cs index 9ba97c880..b2c06eb46 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsCommandMiddlewareTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Comments } [Fact] - public async Task Should_invoke_commands_for_mentioned_users() + public async Task Should_not_invoke_commands_for_mentioned_users() { SetupUser("id1", "mail1@squidex.io"); SetupUser("id2", "mail2@squidex.io"); @@ -101,11 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Comments await sut.HandleAsync(context); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => IsForUser(x, "id1")))) - .MustHaveHappened(); - - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => IsForUser(x, "id2")))) - .MustHaveHappened(); + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs index 1bf78e1a1..959711cce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries new[] { document1.Id }), CreateContent( new[] { document2.Id }, - new[] { document2.Id }), + new[] { document2.Id }) }; A.CallTo(() => assetQuery.QueryAsync(A.That.Matches(x => !x.ShouldEnrichAsset()), null, A.That.Matches(x => x.Ids.Count == 2))) diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs index 91dcc798a..08047aa33 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -118,16 +118,55 @@ namespace Squidex.Domain.Users Assert.False(created); } + [Fact] + public async Task Should_add_claim_when_not_added_yet() + { + var (user, claims) = GenerateUser("id2"); + + A.CallTo(() => userManager.AddClaimsAsync(user, A>._)) + .Returns(IdentityResult.Success); + + SetupUser(user, claims); + + await sut.SetClaimAsync("id2", "my-claim", "new-value"); + + A.CallTo(() => userManager.AddClaimsAsync(user, + A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "new-value")))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_remove_previous_claim() + { + var (user, claims) = GenerateUser("id2"); + + claims.Add(new Claim("my-claim", "old-value")); + + A.CallTo(() => userManager.AddClaimsAsync(user, A>._)) + .Returns(IdentityResult.Success); + + A.CallTo(() => userManager.RemoveClaimsAsync(user, A>._)) + .Returns(IdentityResult.Success); + + SetupUser(user, claims); + + await sut.SetClaimAsync("id2", "my-claim", "new-value"); + + A.CallTo(() => userManager.AddClaimsAsync(user, + A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "new-value")))) + .MustHaveHappened(); + + A.CallTo(() => userManager.RemoveClaimsAsync(user, + A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "old-value")))) + .MustHaveHappened(); + } + [Fact] public async Task Should_resolve_user_by_email() { var (user, claims) = GenerateUser("id1"); - A.CallTo(() => userManager.FindByEmailAsync(user.Email)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); + SetupUser(user, claims); var result = await sut.FindByIdOrEmailAsync(user.Email); @@ -142,11 +181,7 @@ namespace Squidex.Domain.Users { var (user, claims) = GenerateUser("id2"); - A.CallTo(() => userManager.FindByIdAsync(user.Id)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); + SetupUser(user, claims); var result = await sut.FindByIdOrEmailAsync(user.Id)!; @@ -161,11 +196,7 @@ namespace Squidex.Domain.Users { var (user, claims) = GenerateUser("id2"); - A.CallTo(() => userManager.FindByIdAsync(user.Id)) - .Returns(user); - - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); + SetupUser(user, claims); var result = await sut.FindByIdAsync(user.Id)!; @@ -218,6 +249,9 @@ namespace Squidex.Domain.Users A.CallTo(() => userManager.FindByEmailAsync(user.Email)) .Returns(user); + A.CallTo(() => userManager.FindByIdAsync(user.Id)) + .Returns(user); + A.CallTo(() => userManager.GetClaimsAsync(user)) .Returns(claims); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs index 62b36b80f..11fedc967 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.EventSourcing { consumers.Add(new EventConsumerInfo { - Name = "Consumer1", + Name = "Consumer1" }); consumers.Add(new EventConsumerInfo diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index fb3b6aa38..b515e4cae 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -125,7 +125,7 @@ namespace Squidex.Infrastructure.UsageTracking (dateFrom.AddDays(1), new Counters()), (dateFrom.AddDays(2), new Counters()), (dateFrom.AddDays(3), new Counters()), - (dateFrom.AddDays(4), new Counters()), + (dateFrom.AddDays(4), new Counters()) } }; } diff --git a/frontend/app/features/content/pages/content/content-history-page.component.ts b/frontend/app/features/content/pages/content/content-history-page.component.ts index 2505e0aae..7c22761d8 100644 --- a/frontend/app/features/content/pages/content/content-history-page.component.ts +++ b/frontend/app/features/content/pages/content/content-history-page.component.ts @@ -64,7 +64,7 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit this.contentEvents = this.contentsState.selectedContent.pipe( filter(x => !!x), - map(content => `contents.${content?.id}`), + map(content => `schemas.${this.schemasState.schemaId}.contents.${content?.id}`), switchSafe(channel => timer(0, 5000).pipe(map(() => channel))), switchSafe(channel => this.historyService.getHistory(this.appsState.appName, channel))); } diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index 3709dd2e3..2f405a454 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -22,6 +22,8 @@ + +