Skip to content

Commit

Permalink
Add Azure Communication SMS Provider (#15539)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Mike Alhayek <[email protected]>
  • Loading branch information
hishamco and MikeAlhayek authored Sep 16, 2024
1 parent 9931a00 commit 4f4bec4
Show file tree
Hide file tree
Showing 30 changed files with 601 additions and 17 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageVersion Include="AWSSDK.S3" Version="3.7.400.4" />
<PackageVersion Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.301" />
<PackageVersion Include="Azure.Communication.Email" Version="1.0.1" />
<PackageVersion Include="Azure.Communication.Sms" Version="1.0.1" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.1" />
<PackageVersion Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
Expand Down
7 changes: 7 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@
// "DefaultSender": "",
// "ConnectionString": ""
//}
//"OrchardCore_Sms_AzureCommunicationServices": {
// "PhoneNumber": "",
// "ConnectionString": ""
//}
//"OrchardCore_ReverseProxy": {
// "ForwardedHeaders": "None"
//},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AzureSmsSettings>
{
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<AzureSettingsDisplayDriver> htmlLocalizer,
IStringLocalizer<AzureSettingsDisplayDriver> 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<AzureSettingsViewModel>("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<IDisplayResult> 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<SmsSettings>();

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);
}
}
Original file line number Diff line number Diff line change
@@ -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<AzureSmsProviderOptionsConfigurations>()
.AddTransient<IConfigureOptions<AzureSmsOptions>, AzureSmsOptionsConfiguration>();
}
18 changes: 18 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Sms.Azure/Manifest.cs
Original file line number Diff line number Diff line change
@@ -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"
)]
Original file line number Diff line number Diff line change
@@ -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);
}
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 sealed class DefaultAzureSmsOptions : AzureSmsOptions
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

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

The Azure Communication SMS feature enables sending SMS messages via Azure Communication Services (ACS).
</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.ResourceManagement\OrchardCore.ResourceManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Sms.Core\OrchardCore.Sms.Core.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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<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.GetSettingsAsync<AzureSmsSettings>()
.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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AzureSmsOptions> options,
IPhoneFormatValidator phoneFormatValidator,
ILogger<AzureSmsProvider> logger,
IStringLocalizer<AzureSmsProvider> stringLocalizer)
: base(options.Value, phoneFormatValidator, logger, stringLocalizer)
{
}

public override LocalizedString Name
=> S["Azure Communication"];
}
Loading

0 comments on commit 4f4bec4

Please sign in to comment.