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

Merged
merged 34 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d98c12e
Suuport Azure Communication SMS
hishamco Mar 18, 2024
588a0fb
PackageManagement -> PackageVersion
hishamco Sep 13, 2024
2c7bf82
Update the APIs
hishamco Sep 13, 2024
e358db4
Remove module to module dependency
hishamco Sep 13, 2024
d017784
Merge branch 'main' into hishamco/azure-sms
hishamco Sep 13, 2024
955acae
Add project reference
hishamco Sep 13, 2024
1b91442
Add unit test
hishamco Sep 13, 2024
19824f2
Apply suggestions from code review
hishamco Sep 14, 2024
38aba4f
Enhancements
hishamco Sep 14, 2024
693951d
Use SmsResult
hishamco Sep 14, 2024
4dd1256
Fix broken unit test
hishamco Sep 14, 2024
f6559c9
Apply suggestions from code review
hishamco Sep 14, 2024
50df9f5
Fix the build
hishamco Sep 14, 2024
b9abd64
Revert unnecessary changes
hishamco Sep 14, 2024
1c9fd3e
Move SmsPermissions to OC.Sms.Core
hishamco Sep 14, 2024
e208e0c
Address feedback
hishamco Sep 14, 2024
e7d0088
Finalize updates
MikeAlhayek Sep 14, 2024
f482e34
Fix build, improve docs, and fix the default provider
MikeAlhayek Sep 14, 2024
2205984
Fix the UI
MikeAlhayek Sep 14, 2024
fddf697
Updates
hishamco Sep 15, 2024
2f3d045
Update AzureSettingsDisplayDriver.cs
MikeAlhayek Sep 15, 2024
c88451a
Update README.md
MikeAlhayek Sep 15, 2024
11270da
Update 2.1.0.md
MikeAlhayek Sep 15, 2024
3b84c60
Update Manifest.cs
MikeAlhayek Sep 15, 2024
12e06c3
update
MikeAlhayek Sep 15, 2024
93a2dad
Update docs
hishamco Sep 15, 2024
652f93a
Add SMS provider in navigation
hishamco Sep 15, 2024
6b59682
Apply suggestions from code review
hishamco Sep 15, 2024
a63d5f3
Refer to docs in release note
hishamco Sep 15, 2024
3b7d00a
Apply suggestions from code review
hishamco Sep 15, 2024
c6523d5
Apply suggestions from code review
hishamco Sep 15, 2024
1826653
Update AzureSmsProviderBase.cs
MikeAlhayek Sep 15, 2024
124231f
Add docs
hishamco Sep 15, 2024
8b627ca
fix docs
MikeAlhayek Sep 15, 2024
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
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
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 Services")
hishamco marked this conversation as resolved.
Show resolved Hide resolved
.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",
hishamco marked this conversation as resolved.
Show resolved Hide resolved
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 SMS module enables sending SMS messages via Azure Communication Services (ACS).
hishamco marked this conversation as resolved.
Show resolved Hide resolved
</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 Services SMS Provider"];
hishamco marked this conversation as resolved.
Show resolved Hide resolved
}
Loading