Skip to content

Add Azure SMS module #119

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

Merged
merged 2 commits into from
Apr 25, 2024
Merged
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
9 changes: 8 additions & 1 deletion OrchardCoreContrib.Modules.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.System",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Themes.Admin", "src\OrchardCoreContrib.Themes.Admin\OrchardCoreContrib.Themes.Admin.csproj", "{EF11652A-B9A2-4E5E-897E-16F26C4D6946}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Liquid", "src\OrchardCoreContrib.Liquid\OrchardCoreContrib.Liquid.csproj", "{8C49A05B-5D69-4222-9245-310E412E237E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Liquid", "src\OrchardCoreContrib.Liquid\OrchardCoreContrib.Liquid.csproj", "{8C49A05B-5D69-4222-9245-310E412E237E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Sms.Azure", "src\OrchardCoreContrib.Sms.Azure\OrchardCoreContrib.Sms.Azure.csproj", "{47BE63E8-AECC-43E5-AC48-2E2835452402}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -150,6 +152,10 @@ Global
{8C49A05B-5D69-4222-9245-310E412E237E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C49A05B-5D69-4222-9245-310E412E237E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C49A05B-5D69-4222-9245-310E412E237E}.Release|Any CPU.Build.0 = Release|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -177,6 +183,7 @@ Global
{F675A009-CD42-495F-B866-FF917B3D4BDA} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{EF11652A-B9A2-4E5E-897E-16F26C4D6946} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{8C49A05B-5D69-4222-9245-310E412E237E} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{47BE63E8-AECC-43E5-AC48-2E2835452402} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {48F73B05-7D3D-4ACF-81AE-A98B2B4EFDB2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ProjectReference Include="..\OrchardCoreContrib.Html\OrchardCoreContrib.Html.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.Liquid\OrchardCoreContrib.Liquid.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.ReverseProxy.Yarp\OrchardCoreContrib.ReverseProxy.Yarp.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.Sms.Azure\OrchardCoreContrib.Sms.Azure.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.System\OrchardCoreContrib.System.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.Tenants\OrchardCoreContrib.Tenants.csproj" />
<ProjectReference Include="..\OrchardCoreContrib.Themes.Admin\OrchardCoreContrib.Themes.Admin.csproj" />
Expand Down
31 changes: 31 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/AdminMenu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using OrchardCore.Navigation;
using OrchardCoreContrib.Navigation;
using OrchardCoreContrib.Sms.Azure.Drivers;

namespace OrchardCoreContrib.Sms.Azure;

public class AdminMenu(IStringLocalizer<AdminMenu> S) : AdminNavigationProvider
{
private static readonly RouteValueDictionary _routeValues = new()
{
{ "area", "OrchardCore.Settings" },
{ "groupId", AzureSmsSettingsDisplayDriver.GroupId },
};

public override void BuildNavigation(NavigationBuilder builder)
{
builder
.Add(S["Configuration"], configuration => configuration
.Add(S["Settings"], settings => settings
.Add(S["Azure SMS"], S["Azure SMS"].PrefixPosition(), sms => sms
.AddClass("azure-sms").Id("azuresms")
.Action("Index", "Admin", _routeValues)
.Permission(AzureSmsPermissions.ManageSettings)
.LocalNav()
)
)
);
}
}
19 changes: 19 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using OrchardCore.Security.Permissions;

namespace OrchardCoreContrib.Sms.Azure;

public class AzureSmsPermissionProvider : IPermissionProvider
{
private static readonly IEnumerable<Permission> _permissions = [AzureSmsPermissions.ManageSettings];

public Task<IEnumerable<Permission>> GetPermissionsAsync() => Task.FromResult(_permissions);

public IEnumerable<PermissionStereotype> GetDefaultStereotypes() =>
[
new PermissionStereotype
{
Name = "Administrator",
Permissions = _permissions,
}
];
}
8 changes: 8 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using OrchardCore.Security.Permissions;

namespace OrchardCoreContrib.Sms.Azure;

public class AzureSmsPermissions
{
public static readonly Permission ManageSettings = new("ManageSettings", "Manage Azure SMS Settings");
}
8 changes: 8 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/AzureSmsSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace OrchardCoreContrib.Sms.Azure;

public class AzureSmsSettings
{
public string ConnectionString { get; set; }

public string SenderPhoneNumber { get; set; }
}
65 changes: 65 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using OrchardCore.DisplayManagement.Notify;
using OrchardCoreContrib.Sms.Azure.ViewModels;

namespace OrchardCoreContrib.Sms.Azure.Controllers;

public class AdminController : Controller
{
private readonly ISmsService _smsService;
private readonly IAuthorizationService _authorizationService;
private readonly INotifier _notifier;
private readonly IHtmlLocalizer H;

public AdminController(
ISmsService smsService,
IAuthorizationService authorizationService,
INotifier notifier,
IHtmlLocalizer<AdminController> htmlLocalizer)
{
_smsService = smsService;
_authorizationService = authorizationService;
_notifier = notifier;
H = htmlLocalizer;
}

public async Task<IActionResult> Index()
{
if (!await _authorizationService.AuthorizeAsync(User, AzureSmsPermissions.ManageSettings))
{
return Forbid();
}

return View(new AzureSmsSettingsViewModel());
}

[ValidateAntiForgeryToken]
[HttpPost, ActionName(nameof(Index))]
public async Task<IActionResult> IndexPost(AzureSmsSettingsViewModel model)
{
if (!await _authorizationService.AuthorizeAsync(User, AzureSmsPermissions.ManageSettings))
{
return Forbid();
}

if (ModelState.IsValid)
{
var result = await _smsService.SendAsync(model.PhoneNumber, model.Message);

if (result.Succeeded)
{
await _notifier.SuccessAsync(H["The test SMS message has been successfully sent."]);

return RedirectToAction(nameof(Index));
}
else
{
await _notifier.ErrorAsync(H["The test SMS message failed to send."]);
}
}

return View(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using OrchardCore.DisplayManagement.Entities;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Environment.Shell;
using OrchardCore.Settings;

namespace OrchardCoreContrib.Sms.Azure.Drivers;

public class AzureSmsSettingsDisplayDriver(
IDataProtectionProvider dataProtectionProvider,
IHttpContextAccessor httpContextAccessor,
IAuthorizationService authorizationService,
IShellHost shellHost,
ShellSettings shellSettings) : SectionDisplayDriver<ISite, AzureSmsSettings>
{
public const string GroupId = "azure-sms";

private readonly IDataProtectionProvider _dataProtectionProvider = dataProtectionProvider;
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
private readonly IAuthorizationService _authorizationService = authorizationService;
private readonly IShellHost _shellHost = shellHost;
private readonly ShellSettings _shellSettings = shellSettings;

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

if (!await _authorizationService.AuthorizeAsync(user, AzureSmsPermissions.ManageSettings))
{
return null;
}

var shapes = new List<IDisplayResult>
{
Initialize<AzureSmsSettings>("AzureSmsSettings_Edit", model =>
{
model.ConnectionString = settings.ConnectionString;
model.SenderPhoneNumber = settings.SenderPhoneNumber;
}).Location("Content:5").OnGroup(GroupId)
};

if (settings.SenderPhoneNumber is not null)
{
shapes.Add(Dynamic("AzureSmsSettings_TestButton").Location("Actions").OnGroup(GroupId));
}

return Combine(shapes);
}

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

if (!await _authorizationService.AuthorizeAsync(user, AzureSmsPermissions.ManageSettings))
{
return null;
}

if (context.GroupId == GroupId)
{
var previousConnectionString = settings.ConnectionString;
await context.Updater.TryUpdateModelAsync(settings, Prefix);

if (string.IsNullOrWhiteSpace(settings.ConnectionString))
{
settings.ConnectionString = previousConnectionString;
}
else
{
var protector = _dataProtectionProvider.CreateProtector(nameof(AzureSmsSettings));
settings.ConnectionString = protector.Protect(settings.ConnectionString);
}

await _shellHost.ReleaseShellContextAsync(_shellSettings);
}

return await EditAsync(settings, context);
}
}
11 changes: 11 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using OrchardCore.Modules.Manifest;
using ManifestConstants = OrchardCoreContrib.Modules.Manifest.ManifestConstants;

[assembly: Module(
Name = "Azure SMS",
Author = ManifestConstants.Author,
Website = ManifestConstants.Website,
Version = "1.0.0",
Description = "Provides settings and services to send SMS messages using Azure Communication Service.",
Category = "Messaging"
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<ImplicitUsings>enable</ImplicitUsings>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<VersionPrefix>1.0.0</VersionPrefix>
<Authors>The Orchard Core Contrib Team</Authors>
<Company />
<Description>Provides SMS functionality using Azure Communication SMS Services.</Description>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/tree/main/src/OrchardCoreContrib.Sms.Azure/README.md</PackageProjectUrl>
<RepositoryUrl>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageTags>Orchard Core, Orchard Core Contrib, SMS, Azure SMS</PackageTags>
<PackageReleaseNotes>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/releases</PackageReleaseNotes>
<PackageId>OrchardCoreContrib.Sms.Azure</PackageId>
<PackageIcon>icon.png</PackageIcon>
<Product>Orchard Core Contrib Azure SMS Module</Product>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Copyright>2019 Orchard Core Contrib</Copyright>
</PropertyGroup>

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

<ItemGroup>
<None Include="../../images/icon.png" Pack="true" PackagePath="icon.png" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Communication.Sms" Version="1.0.1" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.8.2" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.8.2" />
<PackageReference Include="OrchardCoreContrib.Navigation.Core" Version="1.3.0" />
<PackageReference Include="OrchardCoreContrib.Sms.Abstractions" Version="1.3.0" />
</ItemGroup>

</Project>
45 changes: 45 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/Services/AzureSmsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Azure.Communication.Sms;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCoreContrib.Infrastructure;

namespace OrchardCoreContrib.Sms.Azure.Services;

public class AzureSmsService : ISmsService
{
private readonly AzureSmsSettings _azureSmsOptions;
private readonly ILogger _logger;
private readonly IStringLocalizer S;

public AzureSmsService(
IOptions<AzureSmsSettings> azureSmsOptions,
ILogger<AzureSmsService> logger,
IStringLocalizer<AzureSmsService> stringLocalizer)
{
_azureSmsOptions = azureSmsOptions.Value;
_logger = logger;
S = stringLocalizer;
}

public async Task<SmsResult> SendAsync(SmsMessage message)
{
Guard.ArgumentNotNull(message, nameof(message));

_logger.LogDebug("Attempting to send SMS to {PhoneNumber}.", message.PhoneNumber);

try
{
var client = new SmsClient(_azureSmsOptions.ConnectionString);
var smsResult = await client.SendAsync(_azureSmsOptions.SenderPhoneNumber, message.PhoneNumber, message.Text);

return SmsResult.Success;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while sending an SMS using the Azure SMS.");

return SmsResult.Failed(S["An error occurred while sending an SMS."]);
}
}
}
41 changes: 41 additions & 0 deletions src/OrchardCoreContrib.Sms.Azure/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Admin;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.Modules;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Navigation;
using OrchardCore.Security.Permissions;
using OrchardCore.Settings;
using OrchardCoreContrib.Sms.Azure.Controllers;
using OrchardCoreContrib.Sms.Azure.Drivers;
using OrchardCoreContrib.Sms.Azure.Services;

namespace OrchardCoreContrib.Sms.Azure;

public class Startup(IOptions<AdminOptions> adminOptions) : StartupBase
{
private readonly AdminOptions _adminOptions = adminOptions.Value;

public override void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISmsService, AzureSmsService>();

services.AddScoped<IPermissionProvider, AzureSmsPermissionProvider>();
services.AddScoped<INavigationProvider, AdminMenu>();
services.AddScoped<IDisplayDriver<ISite>, AzureSmsSettingsDisplayDriver>();
}

/// <inheritdoc/>
public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
routes.MapAreaControllerRoute(
name: "AzureSmsTest",
areaName: "OrchardCoreContrib.Sms.Azure",
pattern: _adminOptions.AdminUrlPrefix + "/AzureSms",
defaults: new { controller = typeof(AdminController).ControllerName(), action = nameof(AdminController.Index) }
);
}
}
Loading