Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Azure Communication SMS #15539

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Email.Azure", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Email.Smtp", "src\OrchardCore.Modules\OrchardCore.Email.Smtp\OrchardCore.Email.Smtp.csproj", "{E8A1097D-A65A-4B17-A3A2-F50D79552732}"
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Sms.Twilio", "src\OrchardCore.Modules\OrchardCore.Sms.Twilio\OrchardCore.Sms.Twilio.csproj", "{3AC5FA88-5548-45F3-A14A-B344D32898F7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1371,6 +1375,14 @@ Global
{E8A1097D-A65A-4B17-A3A2-F50D79552732}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8A1097D-A65A-4B17-A3A2-F50D79552732}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8A1097D-A65A-4B17-A3A2-F50D79552732}.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
{3AC5FA88-5548-45F3-A14A-B344D32898F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AC5FA88-5548-45F3-A14A-B344D32898F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AC5FA88-5548-45F3-A14A-B344D32898F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AC5FA88-5548-45F3-A14A-B344D32898F7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1608,6 +1620,8 @@ Global
{47777735-7432-4CCA-A8C5-672E9EE65121} = {90030E85-0C4F-456F-B879-443E8A3F220D}
{C35AB37B-5A09-4896-BEEE-B126B7E7018A} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{E8A1097D-A65A-4B17-A3A2-F50D79552732} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{013C8BBF-6879-4B47-80C9-A466923E45E5} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
{3AC5FA88-5548-45F3-A14A-B344D32898F7} = {A066395F-6F73-45DC-B5A6-B4E306110DCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341}
Expand Down
1 change: 1 addition & 0 deletions src/OrchardCore.Build/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageManagement Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.7" />
<PackageManagement Include="AWSSDK.SecurityToken" Version="3.7.101.60" />
<PackageManagement Include="Azure.Communication.Email" Version="1.0.1" />
<PackageManagement Include="Azure.Communication.Sms" Version="1.0.1" />
<PackageManagement Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.1" />
<PackageManagement Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.3" />
<PackageManagement Include="Azure.Identity" Version="1.10.4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Threading.Tasks;
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.ModelBinding;
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;
using OrchardCore.Sms.Services;

namespace OrchardCore.Sms.Azure.Drivers;

public class AzureSmsSettingsDisplayDriver : SectionDisplayDriver<ISite, AzureSmsSettings>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuthorizationService _authorizationService;
private readonly IPhoneFormatValidator _phoneFormatValidator;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IShellHost _shellHost;
private readonly ShellSettings _shellSettings;
private readonly INotifier _notifier;

protected readonly IHtmlLocalizer H;
protected readonly IStringLocalizer S;

public AzureSmsSettingsDisplayDriver(
IHttpContextAccessor httpContextAccessor,
IAuthorizationService authorizationService,
IPhoneFormatValidator phoneFormatValidator,
IDataProtectionProvider dataProtectionProvider,
IShellHost shellHost,
ShellSettings shellSettings,
INotifier notifier,
IHtmlLocalizer<AzureSmsSettingsDisplayDriver> htmlLocalizer,
IStringLocalizer<AzureSmsSettingsDisplayDriver> stringLocalizer)
{
_httpContextAccessor = httpContextAccessor;
_authorizationService = authorizationService;
_phoneFormatValidator = phoneFormatValidator;
_dataProtectionProvider = dataProtectionProvider;
_shellHost = shellHost;
_shellSettings = shellSettings;
_notifier = notifier;
H = htmlLocalizer;
S = stringLocalizer;
}

public override IDisplayResult Edit(AzureSmsSettings settings)
{
return Initialize<AzureSettingsViewModel>("AzureSmsSettings_Edit", model =>
{
model.IsEnabled = settings.IsEnabled;
model.ConnectionString = settings.ConnectionString;
model.PhoneNumber = settings.PhoneNumber;
}).Location("Content:5#Azure")
.RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageSmsSettings))
.OnGroup(SmsSettings.GroupId);
}

public override async Task<IDisplayResult> UpdateAsync(ISite site, AzureSmsSettings settings, IUpdateModel updater, BuildEditorContext context)
{
var user = _httpContextAccessor.HttpContext?.User;

if (!context.GroupId.Equals(SmsSettings.GroupId, StringComparison.OrdinalIgnoreCase)
|| !await _authorizationService.AuthorizeAsync(user, Permissions.ManageSmsSettings))
{
return null;
}

var model = new AzureSettingsViewModel();

if (await context.Updater.TryUpdateModelAsync(model, Prefix))
{
var hasChanges = settings.IsEnabled != model.IsEnabled;

if (!model.IsEnabled)
{
var smsSettings = site.As<SmsSettings>();

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;

if (string.IsNullOrWhiteSpace(model.ConnectionString))
{
context.Updater.ModelState.AddModelError(Prefix, nameof(model.ConnectionString), S["ConnectionString requires a value."]);
}

if (string.IsNullOrWhiteSpace(model.PhoneNumber))
{
context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Phone number requires a value."]);
}
else if (!_phoneFormatValidator.IsValid(model.PhoneNumber))
{
context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Please provide a valid phone number."]);
}

// Has change should be evaluated before updating the value.
hasChanges |= settings.ConnectionString != model.ConnectionString;
hasChanges |= settings.PhoneNumber != model.PhoneNumber;

if (!string.IsNullOrWhiteSpace(model.ConnectionString))
{
var protector = _dataProtectionProvider.CreateProtector(TwilioSmsProvider.ProtectorName);

var protectedConnectionString = protector.Protect(model.ConnectionString);
hasChanges |= settings.ConnectionString != protectedConnectionString;

settings.ConnectionString = protectedConnectionString;
}

settings.PhoneNumber = model.PhoneNumber;
}

if (context.Updater.ModelState.IsValid && hasChanges)
{
await _shellHost.ReleaseShellContextAsync(_shellSettings);
}
}

return Edit(settings);
}
}
13 changes: 13 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Sms.Azure/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion
)]

[assembly: Feature(
Name = "Azure SMS Provider",
Description = "Provides an SMS service provider leveraging Azure Communication Services (ACS)",
Category = "SMS"
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace OrchardCore.Sms.Azure.Models;

public class AzureSmsOptions
{
public bool IsEnabled { get; set; }

public string ConnectionString { get; set; }

public string PhoneNumber { get; set; }

public bool ConfigurationExists()
=> !string.IsNullOrWhiteSpace(PhoneNumber) && !string.IsNullOrWhiteSpace(ConnectionString);
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace OrchardCore.Sms.Azure.Models;

public class DefaultAzureSmsOptions : AzureSmsOptions
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<!-- NuGet properties-->
<Title>OrchardCore Sms</Title>
<Description>
$(OCCMSDescription)

The SMS module provides a way to send SMS messages.
</Description>
<PackageTags>$(PackageTags) OrchardCoreCMS</PackageTags>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Communication.Sms" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Navigation.Core\OrchardCore.Navigation.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Sms.Core\OrchardCore.Sms.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Workflows.Abstractions\OrchardCore.Workflows.Abstractions.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using OrchardCore.Settings;
using OrchardCore.Sms.Azure.Models;

namespace OrchardCore.Sms.Services;

public class AzureSmsOptionsConfiguration : IConfigureOptions<AzureSmsOptions>
{
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.GetSiteSettingsAsync()
.GetAwaiter()
.GetResult()
.As<AzureSmsSettings>();

options.IsEnabled = settings.IsEnabled;
options.PhoneNumber = settings.PhoneNumber;

if (!string.IsNullOrEmpty(settings.ConnectionString))
{
var protector = _dataProtectionProvider.CreateProtector(ProtectorName);

options.ConnectionString = protector.Unprotect(settings.ConnectionString);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Sms.Azure.Models;

namespace OrchardCore.Sms.Azure.Services;

public class AzureSmsProvider : AzureSmsProviderBase
{
public const string TechnicalName = "Azure";

public AzureSmsProvider(
IOptions<AzureSmsOptions> options,
ILogger<AzureSmsProvider> logger,
IStringLocalizer<AzureSmsProvider> stringLocalizer)
: base(options.Value, logger, stringLocalizer)
{
}

public override LocalizedString DisplayName => S["Azure Communication Service"];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
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 ILogger _logger;

protected readonly IStringLocalizer S;

public AzureSmsProviderBase(
AzureSmsOptions options,
ILogger logger,
IStringLocalizer stringLocalizer)
{
_providerOptions = options;
_logger = logger;
S = stringLocalizer;
}

public abstract LocalizedString DisplayName { get; }

public virtual async Task<SmsResult> SendAsync(SmsMessage message)
{
ArgumentNullException.ThrowIfNull(message);

if (!_providerOptions.IsEnabled)
{
return SmsResult.Failed(S["The Azure Sms Provider is disabled."]);
}

_logger.LogDebug("Attempting to send Sms to {Sms}.", message.To);

try
{
var client = new SmsClient(_providerOptions.ConnectionString);
var smsResult = await client.SendAsync(_providerOptions.PhoneNumber, message.To, message.Body);

return SmsResult.Success;
}
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."]);
}
}
}
Loading
Loading