From 9d4f884efda7e9af8f68e5397fa5da9dfab683b8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 21 Apr 2019 22:55:24 +0200 Subject: [PATCH] Invitations. --- .../HandleRules/EventEnricher.cs | 2 +- .../Apps/Commands/AssignContributor.cs | 2 + .../Invitiation/IInvitationEmailSender.cs | 19 ++++ .../InvitationEmailEventConsumer.cs | 97 +++++++++++++++++ .../Apps/Invitiation/InvitationEmailSender.cs | 102 ++++++++++++++++++ .../Invitiation/InvitationEmailTextOptions.cs | 20 ++++ .../InviteUserCommandMiddleware.cs | 6 +- .../Apps/{ => Invitiation}/InvitedResult.cs | 2 +- .../Backup/RestoreGrain.cs | 2 +- .../Contents/Text/GrainTextIndexer.cs | 3 +- .../IEmailUrlGenerator.cs | 14 +++ .../Rules/UsageTracking/UsageTrackerGrain.cs | 2 +- .../Apps/AppContributorAssigned.cs | 2 + .../CosmosDbEventStore_Reader.cs | 2 +- .../EventSourcing/CosmosDbSubscription.cs | 2 +- .../Email/IEmailSender.cs | 16 +++ .../Email/SmptOptions.cs | 33 ++++++ .../Email/SmtpEmailSender.cs | 42 ++++++++ .../Grains/EventConsumerGrain.cs | 2 +- src/Squidex.Infrastructure/RefToken.cs | 10 ++ src/Squidex.Web/Services/UrlGenerator.cs | 8 +- .../Apps/AppContributorsController.cs | 1 + .../Apps/Models/ContributorAssignedDto.cs | 6 +- src/Squidex/Config/Domain/EntitiesServices.cs | 23 +++- src/Squidex/appsettings.json | 61 +++++++++-- .../RefTokenTests.cs | 10 ++ 26 files changed, 466 insertions(+), 23 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Invitiation/IInvitationEmailSender.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailEventConsumer.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailSender.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailTextOptions.cs rename src/Squidex.Domain.Apps.Entities/Apps/{ => Invitiation}/InviteUserCommandMiddleware.cs (83%) rename src/Squidex.Domain.Apps.Entities/Apps/{ => Invitiation}/InvitedResult.cs (90%) create mode 100644 src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs create mode 100644 src/Squidex.Infrastructure/Email/IEmailSender.cs create mode 100644 src/Squidex.Infrastructure/Email/SmptOptions.cs create mode 100644 src/Squidex.Infrastructure/Email/SmtpEmailSender.cs diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs index f685f1d7e..b8515b1c5 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EventEnricher.cs @@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules user = null; } - if (user == null && actor.Type.Equals(RefTokenType.Client, StringComparison.OrdinalIgnoreCase)) + if (user == null && actor.IsClient) { user = new ClientUser(actor); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs index e7504b1c8..e31a6a55d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs @@ -18,5 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands public bool IsRestore { get; set; } public bool IsInviting { get; set; } + + public bool IsCreated { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/IInvitationEmailSender.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/IInvitationEmailSender.cs new file mode 100644 index 000000000..7f0b46c55 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/IInvitationEmailSender.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation +{ + public interface IInvitationEmailSender + { + Task SendNewUserEmailAsync(IUser assigner, IUser assignee, string appName); + + Task SendExistingUserEmailAsync(IUser assigner, IUser assignee, string appName); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailEventConsumer.cs new file mode 100644 index 000000000..b338f67c1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailEventConsumer.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation +{ + public sealed class InvitationEmailEventConsumer : IEventConsumer + { + private readonly IInvitationEmailSender emailSender; + private readonly IUserResolver userResolver; + private readonly ISemanticLog log; + + public string Name + { + get { return "InvitationEmailSender"; } + } + + public string EventsFilter + { + get { return "^app-"; } + } + + public InvitationEmailEventConsumer(IInvitationEmailSender emailSender, IUserResolver userResolver, ISemanticLog log) + { + Guard.NotNull(emailSender, nameof(emailSender)); + Guard.NotNull(userResolver, nameof(userResolver)); + Guard.NotNull(log, nameof(log)); + + this.emailSender = emailSender; + this.userResolver = userResolver; + + this.log = log; + } + + public bool Handles(StoredEvent @event) + { + return true; + } + + public Task ClearAsync() + { + return TaskHelper.Done; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AppContributorAssigned appContributorAssigned && appContributorAssigned.Actor.IsClient) + { + var assigner = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.Actor.Identifier); + + if (assigner == null) + { + LogWarning($"Assigner {appContributorAssigned.Actor.Identifier} not found"); + return; + } + + var assignee = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.ContributorId); + + if (assignee == null) + { + LogWarning($"Assignee {appContributorAssigned.ContributorId} not found"); + return; + } + + var appName = appContributorAssigned.AppId.Name; + + if (appContributorAssigned.IsCreated) + { + await emailSender.SendNewUserEmailAsync(assigner, assignee, appName); + } + else + { + await emailSender.SendExistingUserEmailAsync(assigner, assignee, appName); + } + } + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailSender.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailSender.cs new file mode 100644 index 000000000..009a22629 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailSender.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation +{ + public sealed class InvitationEmailSender : IInvitationEmailSender + { + private readonly IEmailSender emailSender; + private readonly IEmailUrlGenerator emailUrlGenerator; + private readonly ISemanticLog log; + private readonly InvitationEmailTextOptions texts; + + public InvitationEmailSender( + IOptions texts, + IEmailSender emailSender, + IEmailUrlGenerator emailUrlGenerator, + ISemanticLog log) + { + Guard.NotNull(texts, nameof(texts)); + Guard.NotNull(emailSender, nameof(emailSender)); + Guard.NotNull(emailUrlGenerator, nameof(emailUrlGenerator)); + Guard.NotNull(log, nameof(log)); + + this.texts = texts.Value; + this.emailSender = emailSender; + this.emailUrlGenerator = emailUrlGenerator; + this.log = log; + } + + public Task SendExistingUserEmailAsync(IUser assigner, IUser assignee, string appName) + { + return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); + } + + public Task SendNewUserEmailAsync(IUser assigner, IUser assignee, string appName) + { + return SendEmailAsync(texts.ExistingUserBody, texts.ExistingUserSubject, assigner, assignee, appName); + } + + private async Task SendEmailAsync(string emailBody, string emailSubj, IUser assigner, IUser assignee, string appName) + { + if (string.IsNullOrWhiteSpace(texts.NewUserSubject)) + { + LogWarning("No email subject configured for new users"); + return; + } + + if (string.IsNullOrWhiteSpace(texts.NewUserBody)) + { + LogWarning("No email body configured for new users"); + return; + } + + var appUrl = emailUrlGenerator.GenerateUIUrl(); + + emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); + emailBody = Format(emailBody, assigner, assignee, appUrl, appName); + + await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); + } + + private void LogWarning(string reason) + { + log.LogWarning(w => w + .WriteProperty("action", "InviteUser") + .WriteProperty("status", "Failed") + .WriteProperty("reason", reason)); + } + + private string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) + { + text = text.Replace("$APP_NAME", appName); + + text = text.Replace("$UI_URL", uiUrl); + + if (assigner != null) + { + text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); + text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); + } + + if (assignee != null) + { + text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); + text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); + } + + return text; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailTextOptions.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailTextOptions.cs new file mode 100644 index 000000000..240b3e0a8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitationEmailTextOptions.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation +{ + public sealed class InvitationEmailTextOptions + { + public string NewUserSubject { get; set; } + + public string NewUserBody { get; set; } + + public string ExistingUserSubject { get; set; } + + public string ExistingUserBody { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InviteUserCommandMiddleware.cs similarity index 83% rename from src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InviteUserCommandMiddleware.cs index a82031519..5f77288b7 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/InviteUserCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InviteUserCommandMiddleware.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation { public sealed class InviteUserCommandMiddleware : ICommandMiddleware { @@ -31,11 +31,11 @@ namespace Squidex.Domain.Apps.Entities.Apps { if (assignContributor.IsInviting && assignContributor.ContributorId.IsEmail()) { - var isInvited = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId); + assignContributor.IsCreated = await userResolver.CreateUserIfNotExists(assignContributor.ContributorId); await next(); - if (isInvited && context.PlainResult is EntityCreatedResult id) + if (assignContributor.IsCreated && context.PlainResult is EntityCreatedResult id) { context.Complete(new InvitedResult { Id = id }); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InvitedResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitedResult.cs similarity index 90% rename from src/Squidex.Domain.Apps.Entities/Apps/InvitedResult.cs rename to src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitedResult.cs index 9b599b39c..59dcec9f4 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/InvitedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitiation/InvitedResult.cs @@ -7,7 +7,7 @@ using Squidex.Infrastructure.Commands; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps.Invitiation { public sealed class InvitedResult { diff --git a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index 6f3ac3cb3..9ceec5592 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -239,7 +239,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var actor = CurrentJob.Actor; - if (string.Equals(actor?.Type, RefTokenType.Subject)) + if (actor?.IsSubject == true) { await commandBus.PublishAsync(new AssignContributor { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs index 5f349f651..e4869b4e3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Text/GrainTextIndexer.cs @@ -17,6 +17,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents.Text { @@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text public Task ClearAsync() { - return Task.CompletedTask; + return TaskHelper.Done; } public async Task On(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs b/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs new file mode 100644 index 000000000..98ee82477 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEmailUrlGenerator + { + string GenerateUIUrl(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index ba7773c68..326e6d3fb 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); - return Task.CompletedTask; + return TaskHelper.Done; } public Task ActivateAsync() diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs b/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs index 63f4bba59..64e36ed6b 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs @@ -15,5 +15,7 @@ namespace Squidex.Domain.Apps.Events.Apps public string ContributorId { get; set; } public string Role { get; set; } + + public bool IsCreated { get; set; } } } diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs index e0cccf559..ada171f2a 100644 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Reader.cs @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.EventSourcing ThrowIfDisposed(); - return Task.CompletedTask; + return TaskHelper.Done; } public async Task> QueryAsync(string streamName, long streamPosition = 0) diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs index fa5d8af86..2beb47a61 100644 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs @@ -103,7 +103,7 @@ namespace Squidex.Infrastructure.EventSourcing public Task OpenAsync(IChangeFeedObserverContext context) { - return Task.CompletedTask; + return TaskHelper.Done; } public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) diff --git a/src/Squidex.Infrastructure/Email/IEmailSender.cs b/src/Squidex.Infrastructure/Email/IEmailSender.cs new file mode 100644 index 000000000..dbacfd19d --- /dev/null +++ b/src/Squidex.Infrastructure/Email/IEmailSender.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Email +{ + public interface IEmailSender + { + Task SendAsync(string recipient, string subject, string text); + } +} diff --git a/src/Squidex.Infrastructure/Email/SmptOptions.cs b/src/Squidex.Infrastructure/Email/SmptOptions.cs new file mode 100644 index 000000000..847d9f358 --- /dev/null +++ b/src/Squidex.Infrastructure/Email/SmptOptions.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Email +{ + public sealed class SmptOptions + { + public string Server { get; set; } + + public string Sender { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public bool EnableSsl { get; set; } + + public int Port { get; set; } = 587; + + public bool IsConfigured() + { + return + !string.IsNullOrWhiteSpace(Server) && + !string.IsNullOrWhiteSpace(Sender) && + !string.IsNullOrWhiteSpace(Username) && + !string.IsNullOrWhiteSpace(Password); + } + } +} diff --git a/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs new file mode 100644 index 000000000..1c73283f3 --- /dev/null +++ b/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Net; +using System.Net.Mail; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace Squidex.Infrastructure.Email +{ + public sealed class SmtpEmailSender : IEmailSender + { + private readonly SmtpClient smtpClient; + private readonly string sender; + + public SmtpEmailSender(IOptions options) + { + Guard.NotNull(options, nameof(options)); + + var config = options.Value; + + smtpClient = new SmtpClient(config.Server, config.Port) + { + Credentials = new NetworkCredential( + config.Username, + config.Password), + EnableSsl = config.EnableSsl + }; + + sender = config.Sender; + } + + public Task SendAsync(string recipient, string subject, string body) + { + return smtpClient.SendMailAsync(sender, recipient, subject, body); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index 80fcd9435..f37a9200f 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -53,7 +53,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains eventConsumer = eventConsumerFactory(key); - return Task.CompletedTask; + return TaskHelper.Done; } public Task> GetStateAsync() diff --git a/src/Squidex.Infrastructure/RefToken.cs b/src/Squidex.Infrastructure/RefToken.cs index 5d0c1d215..1d01ba267 100644 --- a/src/Squidex.Infrastructure/RefToken.cs +++ b/src/Squidex.Infrastructure/RefToken.cs @@ -15,6 +15,16 @@ namespace Squidex.Infrastructure public string Identifier { get; } + public bool IsClient + { + get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } + } + + public bool IsSubject + { + get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } + } + public RefToken(string type, string identifier) { Guard.NotNullOrEmpty(type, nameof(type)); diff --git a/src/Squidex.Web/Services/UrlGenerator.cs b/src/Squidex.Web/Services/UrlGenerator.cs index ef2bab32d..5600beaaf 100644 --- a/src/Squidex.Web/Services/UrlGenerator.cs +++ b/src/Squidex.Web/Services/UrlGenerator.cs @@ -9,6 +9,7 @@ using System; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; @@ -19,7 +20,7 @@ using Squidex.Infrastructure.Assets; namespace Squidex.Web.Services { - public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator + public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator { private readonly IAssetStore assetStore; private readonly UrlsOptions urlsOptions; @@ -64,6 +65,11 @@ namespace Squidex.Web.Services return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history"); } + public string GenerateUIUrl() + { + return urlsOptions.BuildUrl("app/"); + } + public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) { return assetStore.GeneratePublicUrl(asset.Id.ToString(), asset.FileVersion, null); diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index d8af9f53b..8b512de21 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -11,6 +11,7 @@ using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Invitiation; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Infrastructure.Commands; using Squidex.Shared; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs index 80d5255d4..871fcc6e9 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs @@ -20,11 +20,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// /// Indicates if the user was created. /// - public bool WasInvited { get; set; } + public bool IsCreated { get; set; } - public static ContributorAssignedDto FromId(string id, bool wasInvited) + public static ContributorAssignedDto FromId(string id, bool isCreated) { - return new ContributorAssignedDto { ContributorId = id, WasInvited = wasInvited }; + return new ContributorAssignedDto { ContributorId = id, IsCreated = isCreated }; } } } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index c99f44e15..84a6e1de3 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -22,6 +22,7 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Apps.Invitiation; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; @@ -44,6 +45,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Email; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Orleans; @@ -63,7 +65,7 @@ namespace Squidex.Config.Domain c.GetRequiredService>(), c.GetRequiredService(), exposeSourceUrl)) - .As().As().As(); + .As().As().As().As(); services.AddSingletonAs() .As().As(); @@ -147,6 +149,25 @@ namespace Squidex.Config.Domain return result; }); + + var emailOptions = config.GetValue("email:smtp"); + + if (emailOptions.IsConfigured()) + { + services.AddSingleton(Options.Create(emailOptions)); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsOptional(); + + services.Configure( + config.GetSection("email:invitations")); + } } private static void AddCommandPipeline(this IServiceCollection services) diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 99f85ff90..28ad6cdc3 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -58,22 +58,69 @@ * * Supported: GoogleMaps, OSM */ - "type": "OSM", - "googleMaps": { - /* + "type": "OSM", + "googleMaps": { + /* * The optional google maps API key. CREATE YOUR OWN PLEASE. */ - "key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg" + "key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg" + } + } + }, + + "email": { + "smtp": { + /* + * The host name to your email server. + */ + "server": "", + /* + * The sender email address. + */ + "sender": "hello@squidex.io", + /* + * The username to authenticate to your email server. + */ + "username": "", + /* + * The password to authenticate to your email server. + */ + "password": "", + /* + * Always use SSL if possible. + */ + "enableSsl": true, + /* + * The port to your email server. + */ + "port": 587 + }, + "invitiations": { + /* + * The email subject when a new user is added as contributor. + */ + "newUserSubject": "Welcome to Squidex, you have been invited to app $APP_NAME", + /* + * The email body when a new user is added as contributor. + */ + "newUserBody": "Welcome\n\nYou have been invited to Squidex CMS by $ASSIGNER_NAME ($ASSIGNER_EMAIL) and to app $", + /* + * The email subject when an existing user is added as contributor. + */ + "existingUserSubject": "[Squidex CMS] You have been invited to app $APP_NAME", + /* + * The email body when an existing user is added as contributor. + */ + "existingUserBody": "" } - } }, "robots": { /* * The text for the robots.txt file */ - "text": "User-agent: *\nAllow: /api/assets/*" - }, + "text": "User-agent: *\nAllow: /api/assets/*" + }, "healthz": { "gc": { diff --git a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs index b7ddf5d99..355aba360 100644 --- a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -30,6 +30,16 @@ namespace Squidex.Infrastructure Assert.Equal("client", token.Type); Assert.Equal("client1", token.Identifier); + + Assert.True(token.IsClient); + } + + [Fact] + public void Should_instantiate_subject_token() + { + var token = new RefToken("subject", "client1"); + + Assert.True(token.IsSubject); } [Fact]