diff --git a/OrchardCoreContrib.Modules.sln b/OrchardCoreContrib.Modules.sln index 7d13573..38866dc 100644 --- a/OrchardCoreContrib.Modules.sln +++ b/OrchardCoreContrib.Modules.sln @@ -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 @@ -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 @@ -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} diff --git a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj index b5f0004..28e890d 100644 --- a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj +++ b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj @@ -28,6 +28,7 @@ + diff --git a/src/OrchardCoreContrib.Sms.Azure/AdminMenu.cs b/src/OrchardCoreContrib.Sms.Azure/AdminMenu.cs new file mode 100644 index 0000000..2506add --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/AdminMenu.cs @@ -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 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() + ) + ) + ); + } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissionProvider.cs b/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissionProvider.cs new file mode 100644 index 0000000..687be67 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissionProvider.cs @@ -0,0 +1,19 @@ +using OrchardCore.Security.Permissions; + +namespace OrchardCoreContrib.Sms.Azure; + +public class AzureSmsPermissionProvider : IPermissionProvider +{ + private static readonly IEnumerable _permissions = [AzureSmsPermissions.ManageSettings]; + + public Task> GetPermissionsAsync() => Task.FromResult(_permissions); + + public IEnumerable GetDefaultStereotypes() => + [ + new PermissionStereotype + { + Name = "Administrator", + Permissions = _permissions, + } + ]; +} diff --git a/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissions.cs b/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissions.cs new file mode 100644 index 0000000..ac6d1f6 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/AzureSmsPermissions.cs @@ -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"); +} diff --git a/src/OrchardCoreContrib.Sms.Azure/AzureSmsSettings.cs b/src/OrchardCoreContrib.Sms.Azure/AzureSmsSettings.cs new file mode 100644 index 0000000..04bba62 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/AzureSmsSettings.cs @@ -0,0 +1,8 @@ +namespace OrchardCoreContrib.Sms.Azure; + +public class AzureSmsSettings +{ + public string ConnectionString { get; set; } + + public string SenderPhoneNumber { get; set; } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/Controllers/AdminController.cs b/src/OrchardCoreContrib.Sms.Azure/Controllers/AdminController.cs new file mode 100644 index 0000000..9e592b2 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Controllers/AdminController.cs @@ -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 htmlLocalizer) + { + _smsService = smsService; + _authorizationService = authorizationService; + _notifier = notifier; + H = htmlLocalizer; + } + + public async Task Index() + { + if (!await _authorizationService.AuthorizeAsync(User, AzureSmsPermissions.ManageSettings)) + { + return Forbid(); + } + + return View(new AzureSmsSettingsViewModel()); + } + + [ValidateAntiForgeryToken] + [HttpPost, ActionName(nameof(Index))] + public async Task 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); + } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/Drivers/AzureSmsSettingsDisplayDriver.cs b/src/OrchardCoreContrib.Sms.Azure/Drivers/AzureSmsSettingsDisplayDriver.cs new file mode 100644 index 0000000..b31a8ec --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Drivers/AzureSmsSettingsDisplayDriver.cs @@ -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 +{ + 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 EditAsync(AzureSmsSettings settings, BuildEditorContext context) + { + var user = _httpContextAccessor.HttpContext?.User; + + if (!await _authorizationService.AuthorizeAsync(user, AzureSmsPermissions.ManageSettings)) + { + return null; + } + + var shapes = new List + { + Initialize("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 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); + } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/Manifest.cs b/src/OrchardCoreContrib.Sms.Azure/Manifest.cs new file mode 100644 index 0000000..06a6b33 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Manifest.cs @@ -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" +)] diff --git a/src/OrchardCoreContrib.Sms.Azure/OrchardCoreContrib.Sms.Azure.csproj b/src/OrchardCoreContrib.Sms.Azure/OrchardCoreContrib.Sms.Azure.csproj new file mode 100644 index 0000000..5a3e044 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/OrchardCoreContrib.Sms.Azure.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + true + enable + true + 1.0.0 + The Orchard Core Contrib Team + + Provides SMS functionality using Azure Communication SMS Services. + BSD-3-Clause + https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/tree/main/src/OrchardCoreContrib.Sms.Azure/README.md + https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules + git + true + Orchard Core, Orchard Core Contrib, SMS, Azure SMS + https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/releases + OrchardCoreContrib.Sms.Azure + icon.png + Orchard Core Contrib Azure SMS Module + true + 2019 Orchard Core Contrib + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCoreContrib.Sms.Azure/Services/AzureSmsService.cs b/src/OrchardCoreContrib.Sms.Azure/Services/AzureSmsService.cs new file mode 100644 index 0000000..e930c89 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Services/AzureSmsService.cs @@ -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 azureSmsOptions, + ILogger logger, + IStringLocalizer stringLocalizer) + { + _azureSmsOptions = azureSmsOptions.Value; + _logger = logger; + S = stringLocalizer; + } + + public async Task 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."]); + } + } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/Startup.cs b/src/OrchardCoreContrib.Sms.Azure/Startup.cs new file mode 100644 index 0000000..5a899bc --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Startup.cs @@ -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) : StartupBase +{ + private readonly AdminOptions _adminOptions = adminOptions.Value; + + public override void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped, AzureSmsSettingsDisplayDriver>(); + } + + /// + 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) } + ); + } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/ViewModels/AzureSmsSettingsViewModel.cs b/src/OrchardCoreContrib.Sms.Azure/ViewModels/AzureSmsSettingsViewModel.cs new file mode 100644 index 0000000..92c038f --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/ViewModels/AzureSmsSettingsViewModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace OrchardCoreContrib.Sms.Azure.ViewModels; + +public class AzureSmsSettingsViewModel +{ + [Required] + public string PhoneNumber { get; set; } + + [Required] + public string Message { get; set; } +} diff --git a/src/OrchardCoreContrib.Sms.Azure/Views/Admin/Index.cshtml b/src/OrchardCoreContrib.Sms.Azure/Views/Admin/Index.cshtml new file mode 100644 index 0000000..7ac3fd8 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Views/Admin/Index.cshtml @@ -0,0 +1,22 @@ +@using OrchardCoreContrib.Sms.Azure.ViewModels +@model AzureSmsSettingsViewModel + +

@RenderTitleSegments(T["Test Azure SMS Settings"])

+ +
+ @Html.ValidationSummary() + +
+ + + +
+
+ + + +
+
+ +
+
diff --git a/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml b/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml new file mode 100644 index 0000000..a758d76 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.Edit.cshtml @@ -0,0 +1,19 @@ +@using OrchardCoreContrib.Sms.Azure + +@model AzureSmsSettings + +

@T["The current tenant will be reloaded when the settings are saved."]

+ +
+ + + @T["The phone number that provided by Azure Communication Services (ACS) to send the SMS."] + +
+ +
+ + + + @T["The connection string that provided by Azure Communication Services (ACS)."] +
diff --git a/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.TestButton.cshtml b/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.TestButton.cshtml new file mode 100644 index 0000000..14097f2 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Views/AzureSmsSettings.TestButton.cshtml @@ -0,0 +1 @@ +@T["Test settings"] diff --git a/src/OrchardCoreContrib.Sms.Azure/Views/NavigationItemText-azuresms.Id.cshtml b/src/OrchardCoreContrib.Sms.Azure/Views/NavigationItemText-azuresms.Id.cshtml new file mode 100644 index 0000000..f30e615 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Views/NavigationItemText-azuresms.Id.cshtml @@ -0,0 +1 @@ +@T["Azure SMS"] diff --git a/src/OrchardCoreContrib.Sms.Azure/Views/_ViewImports.cshtml b/src/OrchardCoreContrib.Sms.Azure/Views/_ViewImports.cshtml new file mode 100644 index 0000000..252fd65 --- /dev/null +++ b/src/OrchardCoreContrib.Sms.Azure/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement