Skip to content

Commit

Permalink
Add Azure SMS module
Browse files Browse the repository at this point in the history
  • Loading branch information
hishamco committed Apr 25, 2024
1 parent 4cc073d commit 17acb6f
Show file tree
Hide file tree
Showing 18 changed files with 417 additions and 1 deletion.
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,38 @@
<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>
<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

0 comments on commit 17acb6f

Please sign in to comment.