diff --git a/Directory.Packages.props b/Directory.Packages.props index 110df2fa9a0..f433d8e3de8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + diff --git a/OrchardCore.sln b/OrchardCore.sln index e2b57d6f876..172cbd5cfc9 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -525,6 +525,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Rules.Core", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Queries.Core", "src\OrchardCore\OrchardCore.Queries.Core\OrchardCore.Queries.Core.csproj", "{61B358F2-702C-40AA-9DF7-7121248FE6DE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Sms.Azure", "src\OrchardCore.Modules\OrchardCore.Sms.Azure\OrchardCore.Sms.Azure.csproj", "{013C8BBF-6879-4B47-80C9-A466923E45E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1389,6 +1391,10 @@ Global {61B358F2-702C-40AA-9DF7-7121248FE6DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {61B358F2-702C-40AA-9DF7-7121248FE6DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {61B358F2-702C-40AA-9DF7-7121248FE6DE}.Release|Any CPU.Build.0 = Release|Any CPU + {013C8BBF-6879-4B47-80C9-A466923E45E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {013C8BBF-6879-4B47-80C9-A466923E45E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {013C8BBF-6879-4B47-80C9-A466923E45E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {013C8BBF-6879-4B47-80C9-A466923E45E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1629,6 +1635,7 @@ Global {E8A1097D-A65A-4B17-A3A2-F50D79552732} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} {4BAA08A2-878C-4B96-86BF-5B3DB2B6C2C7} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {61B358F2-702C-40AA-9DF7-7121248FE6DE} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} + {013C8BBF-6879-4B47-80C9-A466923E45E5} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341} diff --git a/mkdocs.yml b/mkdocs.yml index c8945c96cd7..de7f8b0f150 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -196,6 +196,7 @@ nav: - Shortcodes: reference/modules/Shortcodes/README.md - Sitemaps: reference/modules/Sitemaps/README.md - SMS: reference/modules/Sms/README.md + - Azure Communication SMS: reference/modules/Sms.Azure/README.md - Spatial: reference/modules/Spatial/README.md - XML-RPC: reference/modules/XmlRpc/README.md - Menu: reference/modules/Menu/README.md diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index a4e2b9e1074..774dd51b4ae 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -254,6 +254,10 @@ // "DefaultSender": "", // "ConnectionString": "" //} + //"OrchardCore_Sms_AzureCommunicationServices": { + // "PhoneNumber": "", + // "ConnectionString": "" + //} //"OrchardCore_ReverseProxy": { // "ForwardedHeaders": "None" //}, diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Drivers/AzureSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Drivers/AzureSettingsDisplayDriver.cs new file mode 100644 index 00000000000..cda64d20199 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Drivers/AzureSettingsDisplayDriver.cs @@ -0,0 +1,145 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; +using OrchardCore.Mvc.ModelBinding; +using OrchardCore.Settings; +using OrchardCore.Sms.Azure.Models; +using OrchardCore.Sms.Azure.Services; +using OrchardCore.Sms.Azure.ViewModels; + +namespace OrchardCore.Sms.Azure.Drivers; + +public sealed class AzureSettingsDisplayDriver : SiteDisplayDriver +{ + private readonly IShellReleaseManager _shellReleaseManager; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IPhoneFormatValidator _phoneFormatValidator; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly INotifier _notifier; + + internal readonly IHtmlLocalizer H; + internal readonly IStringLocalizer S; + + protected override string SettingsGroupId + => SmsSettings.GroupId; + + public AzureSettingsDisplayDriver( + IShellReleaseManager shellReleaseManager, + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IPhoneFormatValidator phoneFormatValidator, + IDataProtectionProvider dataProtectionProvider, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) + { + _shellReleaseManager = shellReleaseManager; + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _phoneFormatValidator = phoneFormatValidator; + _dataProtectionProvider = dataProtectionProvider; + _notifier = notifier; + H = htmlLocalizer; + S = stringLocalizer; + } + + public override IDisplayResult Edit(ISite site, AzureSmsSettings settings, BuildEditorContext c) + { + return Initialize("AzureSmsSettings_Edit", model => + { + model.IsEnabled = settings.IsEnabled; + model.PhoneNumber = settings.PhoneNumber; + model.HasConnectionString = !string.IsNullOrEmpty(settings.ConnectionString); + }).Location("Content:5#Azure Communication") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, SmsPermissions.ManageSmsSettings)) + .OnGroup(SettingsGroupId); + } + + public override async Task UpdateAsync(ISite site, AzureSmsSettings settings, UpdateEditorContext context) + { + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, SmsPermissions.ManageSmsSettings)) + { + return null; + } + + var model = new AzureSettingsViewModel(); + + await context.Updater.TryUpdateModelAsync(model, Prefix); + + var smsSettings = site.As(); + + var hasChanges = settings.IsEnabled != model.IsEnabled; + if (!model.IsEnabled) + { + if (hasChanges && smsSettings.DefaultProviderName == AzureSmsProvider.TechnicalName) + { + await _notifier.WarningAsync(H["You have successfully disabled the default SMS provider. The SMS service is now disable and will remain disabled until you designate a new default provider."]); + + smsSettings.DefaultProviderName = null; + + site.Put(smsSettings); + } + + settings.IsEnabled = false; + } + else + { + settings.IsEnabled = true; + + hasChanges |= model.PhoneNumber != settings.PhoneNumber; + + if (string.IsNullOrEmpty(model.PhoneNumber)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["The phone number is a required."]); + } + else if (!_phoneFormatValidator.IsValid(model.PhoneNumber)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Invalid phone number."]); + } + + settings.PhoneNumber = model.PhoneNumber; + + if (string.IsNullOrWhiteSpace(model.ConnectionString) && settings.ConnectionString is null) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.ConnectionString), S["Connection string is required."]); + } + else if (!string.IsNullOrWhiteSpace(model.ConnectionString)) + { + var protector = _dataProtectionProvider.CreateProtector(AzureSmsOptionsConfiguration.ProtectorName); + + var protectedConnection = protector.Protect(model.ConnectionString); + + // Check if the connection string changed before setting it. + hasChanges |= protectedConnection != settings.ConnectionString; + + settings.ConnectionString = protectedConnection; + } + } + + if (context.Updater.ModelState.IsValid && settings.IsEnabled && string.IsNullOrEmpty(smsSettings.DefaultProviderName)) + { + // If we are enabling the only provider, set it as the default one. + smsSettings.DefaultProviderName = AzureSmsProvider.TechnicalName; + site.Put(smsSettings); + + hasChanges = true; + } + + if (hasChanges) + { + _shellReleaseManager.RequestRelease(); + } + + return Edit(site, settings, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Extensions/SmsProviderExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Extensions/SmsProviderExtensions.cs new file mode 100644 index 00000000000..8e34e0bb340 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Extensions/SmsProviderExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Sms.Azure.Models; +using OrchardCore.Sms.Azure.Services; + +namespace OrchardCore.Sms.Azure; + +public static class SmsProviderExtensions +{ + public static IServiceCollection AddAzureSmsProvider(this IServiceCollection services) + => services.AddSmsProviderOptionsConfiguration() + .AddTransient, AzureSmsOptionsConfiguration>(); +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Manifest.cs new file mode 100644 index 00000000000..2c2d8819274 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Manifest.cs @@ -0,0 +1,18 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion +)] + +[assembly: Feature( + Name = "Azure Communication SMS", + Id = "OrchardCore.Sms.Azure", + Description = "Enables the ability to send SMS messages through Azure Communication Services (ACS).", + Dependencies = + [ + "OrchardCore.Sms", + ], + Category = "SMS" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsOptions.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsOptions.cs new file mode 100644 index 00000000000..8a0407dcb42 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsOptions.cs @@ -0,0 +1,14 @@ +namespace OrchardCore.Sms.Azure.Models; + +public class AzureSmsOptions +{ + public bool IsEnabled { get; set; } + + public string PhoneNumber { get; set; } + + public string ConnectionString { get; set; } + + public bool ConfigurationExists() + => !string.IsNullOrWhiteSpace(PhoneNumber) && + !string.IsNullOrWhiteSpace(ConnectionString); +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsSettings.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsSettings.cs new file mode 100644 index 00000000000..2f5b4cfdd01 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/AzureSmsSettings.cs @@ -0,0 +1,10 @@ +namespace OrchardCore.Sms.Azure.Models; + +public class AzureSmsSettings +{ + public bool IsEnabled { get; set; } + + public string ConnectionString { get; set; } + + public string PhoneNumber { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/DefaultAzureSmsOptions.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/DefaultAzureSmsOptions.cs new file mode 100644 index 00000000000..06ed3434986 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Models/DefaultAzureSmsOptions.cs @@ -0,0 +1,5 @@ +namespace OrchardCore.Sms.Azure.Models; + +public sealed class DefaultAzureSmsOptions : AzureSmsOptions +{ +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/OrchardCore.Sms.Azure.csproj b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/OrchardCore.Sms.Azure.csproj new file mode 100644 index 00000000000..b6f920f99c4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/OrchardCore.Sms.Azure.csproj @@ -0,0 +1,29 @@ + + + + true + + OrchardCore Sms + + $(OCCMSDescription) + + The Azure Communication SMS feature enables sending SMS messages via Azure Communication Services (ACS). + + $(PackageTags) OrchardCoreCMS + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsOptionsConfiguration.cs new file mode 100644 index 00000000000..31cb81916e6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsOptionsConfiguration.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; +using OrchardCore.Settings; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure.Services; + +public sealed class AzureSmsOptionsConfiguration : IConfigureOptions +{ + public const string ProtectorName = "AzureSmsProtector"; + + private readonly ISiteService _siteService; + private readonly IDataProtectionProvider _dataProtectionProvider; + + public AzureSmsOptionsConfiguration( + ISiteService siteService, + IDataProtectionProvider dataProtectionProvider) + { + _siteService = siteService; + _dataProtectionProvider = dataProtectionProvider; + } + + public void Configure(AzureSmsOptions options) + { + var settings = _siteService.GetSettingsAsync() + .GetAwaiter() + .GetResult(); + + options.IsEnabled = settings.IsEnabled; + options.PhoneNumber = settings.PhoneNumber; + + if (!string.IsNullOrEmpty(settings.ConnectionString)) + { + var protector = _dataProtectionProvider.CreateProtector(ProtectorName); + + options.ConnectionString = protector.Unprotect(settings.ConnectionString); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProvider.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProvider.cs new file mode 100644 index 00000000000..b892c0664a5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure.Services; + +public sealed class AzureSmsProvider : AzureSmsProviderBase +{ + public const string TechnicalName = "Azure"; + + public AzureSmsProvider( + IOptions options, + IPhoneFormatValidator phoneFormatValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, phoneFormatValidator, logger, stringLocalizer) + { + } + + public override LocalizedString Name + => S["Azure Communication"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderBase.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderBase.cs new file mode 100644 index 00000000000..d08119d22e5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderBase.cs @@ -0,0 +1,78 @@ +using Azure.Communication.Sms; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure.Services; + +public abstract class AzureSmsProviderBase : ISmsProvider +{ + private readonly AzureSmsOptions _providerOptions; + private readonly IPhoneFormatValidator _phoneFormatValidator; + private readonly ILogger _logger; + + private SmsClient _smsClient; + + protected readonly IStringLocalizer S; + + public AzureSmsProviderBase( + AzureSmsOptions options, + IPhoneFormatValidator phoneFormatValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + { + _providerOptions = options; + _phoneFormatValidator = phoneFormatValidator; + _logger = logger; + S = stringLocalizer; + } + + public abstract LocalizedString Name { get; } + + public virtual async Task SendAsync(SmsMessage message) + { + ArgumentNullException.ThrowIfNull(message); + + if (!_providerOptions.IsEnabled) + { + return SmsResult.Failed(S["The Azure Communication Provider is disabled."]); + } + + _logger.LogDebug("Attempting to send an SMS message using Azure Communication service to {Recipient}.", message.To); + + if (string.IsNullOrWhiteSpace(message.To)) + { + return SmsResult.Failed(S["A phone number is required for the recipient.", message.To]); + } + + if (!_phoneFormatValidator.IsValid(message.To)) + { + return SmsResult.Failed(S["Invalid phone number format for the recipient: '{0}'.", message.To]); + } + + if (string.IsNullOrEmpty(message.Body)) + { + return SmsResult.Failed(S["The message body is required.", message.To]); + } + + try + { + _smsClient ??= new SmsClient(_providerOptions.ConnectionString); + + var response = await _smsClient.SendAsync(_providerOptions.PhoneNumber, message.To, message.Body); + + if (response.Value.Successful) + { + return SmsResult.Success; + } + + return SmsResult.Failed(S["SMS message was not send."]); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while sending an SMS using the Azure SMS Provider."); + + return SmsResult.Failed(S["An error occurred while sending an SMS."]); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderOptionsConfigurations.cs new file mode 100644 index 00000000000..59a262017d8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/AzureSmsProviderOptionsConfigurations.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure.Services; + +public sealed class AzureSmsProviderOptionsConfigurations : IConfigureOptions +{ + private readonly AzureSmsOptions _azureOptions; + private readonly DefaultAzureSmsOptions _defaultAzureOptions; + + public AzureSmsProviderOptionsConfigurations( + IOptions azureOptions, + IOptions defaultAzureOptions) + { + _azureOptions = azureOptions.Value; + _defaultAzureOptions = defaultAzureOptions.Value; + } + + public void Configure(SmsProviderOptions options) + { + ConfigureTenantProvider(options); + + if (_defaultAzureOptions.IsEnabled) + { + // Configure the default provider only if settings are supplied by the configuration provider. + ConfigureDefaultProvider(options); + } + } + + private void ConfigureTenantProvider(SmsProviderOptions options) + { + var typeOptions = new SmsProviderTypeOptions(typeof(AzureSmsProvider)) + { + IsEnabled = _azureOptions.IsEnabled, + }; + + options.TryAddProvider(AzureSmsProvider.TechnicalName, typeOptions); + } + + private static void ConfigureDefaultProvider(SmsProviderOptions options) + { + var typeOptions = new SmsProviderTypeOptions(typeof(DefaultAzureSmsProvider)) + { + IsEnabled = true, + }; + + options.TryAddProvider(DefaultAzureSmsProvider.TechnicalName, typeOptions); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/DefaultAzureSmsProvider.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/DefaultAzureSmsProvider.cs new file mode 100644 index 00000000000..c408da75f1c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Services/DefaultAzureSmsProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure.Services; + +public sealed class DefaultAzureSmsProvider : AzureSmsProviderBase +{ + public const string TechnicalName = "DefaultAzure"; + + public DefaultAzureSmsProvider( + IOptions options, + IPhoneFormatValidator phoneFormatValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, phoneFormatValidator, logger, stringLocalizer) + { + } + + public override LocalizedString Name + => S["Default Azure Communication"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Startup.cs new file mode 100644 index 00000000000..8b899739ac3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Startup.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Modules; +using OrchardCore.Sms.Azure.Drivers; +using OrchardCore.Sms.Azure.Models; + +namespace OrchardCore.Sms.Azure; + +public sealed class Startup : StartupBase +{ + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) + { + _shellConfiguration = shellConfiguration; + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddAzureSmsProvider() + .AddSiteDisplayDriver(); + + services.Configure(options => + { + _shellConfiguration.GetSection("OrchardCore_Sms_AzureCommunicationServices").Bind(options); + + options.IsEnabled = options.ConfigurationExists(); + }); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/ViewModels/AzureSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/ViewModels/AzureSettingsViewModel.cs new file mode 100644 index 00000000000..8626f12a78a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/ViewModels/AzureSettingsViewModel.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using OrchardCore.Sms.ViewModels; + +namespace OrchardCore.Sms.Azure.ViewModels; + +public class AzureSettingsViewModel : SmsSettingsBaseViewModel +{ + public bool IsEnabled { get; set; } + + public string ConnectionString { get; set; } + + public string PhoneNumber { get; set; } + + [BindNever] + public bool HasConnectionString { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml new file mode 100644 index 00000000000..39a2938728a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml @@ -0,0 +1,29 @@ +@using OrchardCore.Sms.Azure.Services +@using OrchardCore.Sms.Azure.ViewModels +@using OrchardCore.Sms + +@model AzureSettingsViewModel + +
+
+ + +
+
+ +
+ +
+ + + + @T["The default phone number to use as a sender. Phone number must include a country code. For example, +1 for United States."] +
+ +
+ + + +
+ +
diff --git a/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..252fd654bb8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms.Azure/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs index aa35378721f..c0f65f8fc88 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; @@ -8,7 +7,6 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; -using OrchardCore.Mvc.ModelBinding; using OrchardCore.Settings; using OrchardCore.Sms.ViewModels; @@ -68,18 +66,11 @@ public override async Task UpdateAsync(ISite site, SmsSettings s await context.Updater.TryUpdateModelAsync(model, Prefix); - if (string.IsNullOrEmpty(model.DefaultProvider)) + if (settings.DefaultProviderName != model.DefaultProvider) { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultProvider), S["You must select a default provider."]); - } - else - { - if (settings.DefaultProviderName != model.DefaultProvider) - { - settings.DefaultProviderName = model.DefaultProvider; + settings.DefaultProviderName = model.DefaultProvider; - _shellReleaseManager.RequestRelease(); - } + _shellReleaseManager.RequestRelease(); } return Edit(site, settings, context); diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs index 96e611ebb3e..d6651606d9f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs @@ -79,11 +79,10 @@ public override async Task UpdateAsync(ISite site, TwilioSetting await context.Updater.TryUpdateModelAsync(model, Prefix); var hasChanges = settings.IsEnabled != model.IsEnabled; + var smsSettings = site.As(); if (!model.IsEnabled) { - var smsSettings = site.As(); - if (hasChanges && smsSettings.DefaultProviderName == TwilioSmsProvider.TechnicalName) { await _notifier.WarningAsync(H["You have successfully disabled the default SMS provider. The SMS service is now disable and will remain disabled until you designate a new default provider."]); @@ -136,6 +135,16 @@ public override async Task UpdateAsync(ISite site, TwilioSetting } } + if (context.Updater.ModelState.IsValid && settings.IsEnabled && string.IsNullOrEmpty(smsSettings.DefaultProviderName)) + { + // If we are enabling the only provider, set it as the default one. + smsSettings.DefaultProviderName = TwilioSmsProvider.TechnicalName; + + site.Put(smsSettings); + + hasChanges = true; + } + if (hasChanges) { _shellReleaseManager.RequestRelease(); diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissionProvider.cs b/src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissionProvider.cs index 50207b1cf78..91b277c394b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissionProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissionProvider.cs @@ -4,11 +4,12 @@ namespace OrchardCore.Sms; public sealed class SmsPermissionProvider : IPermissionProvider { + [Obsolete("This should not be used. Instead use SmsPermissions.ManageSmsSettings")] public static readonly Permission ManageSmsSettings = SmsPermissions.ManageSmsSettings; private readonly IEnumerable _allPermissions = [ - ManageSmsSettings, + SmsPermissions.ManageSmsSettings, ]; public Task> GetPermissionsAsync() diff --git a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj index 156ba96f7a6..252fde56425 100644 --- a/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj +++ b/src/OrchardCore/OrchardCore.Application.Cms.Core.Targets/OrchardCore.Application.Cms.Core.Targets.csproj @@ -114,6 +114,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissions.cs b/src/OrchardCore/OrchardCore.Sms.Core/SmsPermissions.cs similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Sms/SmsPermissions.cs rename to src/OrchardCore/OrchardCore.Sms.Core/SmsPermissions.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsSettingsBaseViewModel.cs b/src/OrchardCore/OrchardCore.Sms.Core/ViewModels/SmsSettingsBaseViewModel.cs similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsSettingsBaseViewModel.cs rename to src/OrchardCore/OrchardCore.Sms.Core/ViewModels/SmsSettingsBaseViewModel.cs diff --git a/src/docs/reference/README.md b/src/docs/reference/README.md index 47b3823cc12..38bdd154314 100644 --- a/src/docs/reference/README.md +++ b/src/docs/reference/README.md @@ -122,6 +122,7 @@ Here's a categorized overview of all built-in Orchard Core features at a glance. - [Diagnostics](modules/Diagnostics/README.md) - [Remote Deployment](modules/Deployment.Remote/README.md) - [Sms](modules/Sms/README.md) +- [Azure Communication SMS](modules/Sms.Azure/README.md) ### Localization diff --git a/src/docs/reference/modules/Sms.Azure/README.md b/src/docs/reference/modules/Sms.Azure/README.md new file mode 100644 index 00000000000..a0178ea9ae1 --- /dev/null +++ b/src/docs/reference/modules/Sms.Azure/README.md @@ -0,0 +1,29 @@ +# Azure SMS (`OrchardCore.Sms.Azure`) + +This feature provides SMS providers for sending SMS through [Azure Communication Services SMS](https://learn.microsoft.com/en-us/azure/communication-services/concepts/sms/concepts). + +## **Azure Communication** Provider Configuration + +Enabling this feature will introduce a new tab labeled **Azure** within the SMS settings, allowing you to configure the service. To access these settings from the admin dashboard, navigate to `Configuration` → `Settings` → `Sms` and click on the 'Azure Communication' tab. The following are the available settings. + +| Provider | Description | +| --- | --- | +| `Azure` | This provider enables tenant-specific Azure Communication Services for sending SMS. Configure the SMS settings to activate this provider. | +| `DefaultAzure` | This provider sets default Azure Communication Service configurations for all tenants.| + + +## **Default Azure Communication** Provider Configuration + +You may configure the **Default Azure Communication** using any configuration provider via the following settings: + +```json +"OrchardCore_Sms_AzureCommunicationServices": { + "PhoneNumber": "", + "ConnectionString": "" +} +``` + +For more information about configurations, please refer to [Configuration](../../core/Configuration/README.md). + +!!! note + Configuration of the **Default Azure Communication** provider cannot be performed through Admin Settings. Instead, use the configuration provider for setup. Note that the provider will only appear if the configuration is present. diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index 482f38a81cc..dae4526b949 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -14,15 +14,20 @@ Enabling the `SMS` feature will add a new settings page under `Configurations` > !!! note After enabling the SMS feature, you must configure the default provider in order to send SMS messages. -# Configuring Twilio Provider +## Configuring the Twilio Providers To enable the [Twilio](https://www.twilio.com) provider, navigate to `Configurations` >> `Settings` >> `SMS`. Click on the `Twilio` tab, click the Enable checkbox and provider your Twilio account info. Then in the `Providers` tab, select Twilio as your default provider. +## Additional Available Providers + +- [Azure Communication](../reference/modules/Sms.Azure/README.md) service provider. + ## Adding Custom Providers The `OrchardCore.Sms` module provides you with the capability to integrate additional providers for dispatching SMS messages. To achieve this, you can easily create an implementation of the `ISmsProvider` interface and then proceed to register it using one of the following approaches: If your provider does not require any settings like the `LogProvider`, you may register it like this. + ```csharp services.AddSmsProvider("A technical name for your implementation") ``` @@ -77,7 +82,7 @@ public class TestController { var message = new SmsMessage { - To = "17023451234", + To = "+17023451234", Message = "It's easy to send an SMS message using Orchard!", }; diff --git a/src/docs/releases/2.1.0.md b/src/docs/releases/2.1.0.md index 96d4d7e3a43..fd7b3458ad6 100644 --- a/src/docs/releases/2.1.0.md +++ b/src/docs/releases/2.1.0.md @@ -1,3 +1,9 @@ # Orchard Core 2.1.0 Release date: Not yet released + +## Change Logs + +### New 'Azure Communication SMS' feature + +A new feature was added to allow you to send SMS messages using Azure Communication Services (ACS). Simply enable it then navigate to the admin dashboard > `Configurations` >> `Settings` >> `SMS` to configure the provider. For more information you can refer to the [docs](../reference/modules/Sms.Azure/README.md).