From 54f194f6a223fd99211983a4e537ea13125393df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartha?= Date: Wed, 15 Feb 2023 17:53:07 +0100 Subject: [PATCH 1/6] Adding reset password token task --- .../GenerateResetPasswordTokenTask.cs | 74 +++++++++++++++++++ ...rateResetPasswordTokenTaskDisplayDriver.cs | 11 +++ .../Extensions/Workflows/Startup.cs | 14 ++++ ...GenerateResetPasswordTokenTaskViewModel.cs | 5 ++ Lombiq.HelpfulExtensions/FeatureIds.cs | 1 + .../Lombiq.HelpfulExtensions.csproj | 1 + Lombiq.HelpfulExtensions/Manifest.cs | 11 +++ ...esetPasswordTokenTask.Fields.Design.cshtml | 8 ++ ...eResetPasswordTokenTask.Fields.Edit.cshtml | 0 ...tPasswordTokenTask.Fields.Thumbnail.cshtml | 2 + Readme.md | 4 + 11 files changed, 131 insertions(+) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs create mode 100644 Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs create mode 100644 Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml create mode 100644 Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml create mode 100644 Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Thumbnail.cshtml diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs new file mode 100644 index 00000000..67eae847 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Localization; +using OrchardCore.Users; +using OrchardCore.Users.Models; +using OrchardCore.Users.Services; +using OrchardCore.Workflows.Abstractions.Models; +using OrchardCore.Workflows.Activities; +using OrchardCore.Workflows.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; + +public class GenerateResetPasswordTokenTask : TaskActivity +{ + private readonly IStringLocalizer T; + private readonly LinkGenerator _linkGenerator; + private readonly IHttpContextAccessor _hca; + private readonly UserManager _userManager; + private readonly IUserService _userService; + + public override string Name => nameof(GenerateResetPasswordTokenTask); + public override LocalizedString DisplayText => T["Generate reset password token"]; + public override LocalizedString Category => T["User"]; + + public GenerateResetPasswordTokenTask( + IStringLocalizer localizer, + LinkGenerator linkGenerator, + IHttpContextAccessor hca, + UserManager userManager, + IUserService userService) + { + T = localizer; + _linkGenerator = linkGenerator; + _hca = hca; + _userManager = userManager; + _userService = userService; + } + + public override IEnumerable GetPossibleOutcomes( + WorkflowExecutionContext workflowContext, + ActivityContext activityContext) => + Outcomes(T["Done"]); + + public override async Task ExecuteAsync( + WorkflowExecutionContext workflowContext, + ActivityContext activityContext) + { + var user = workflowContext.Input["User"] as User ?? workflowContext.Properties["User"] as User; + if (user == null && _hca.HttpContext.User.Identity.IsAuthenticated) + { + user = await _userService.GetAuthenticatedUserAsync(_hca.HttpContext.User) as User; + } + + if (user == null) return Outcomes("Done", "Done"); + + var generatedToken = await _userManager.GeneratePasswordResetTokenAsync(user); + user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(generatedToken)); + workflowContext.Properties["ResetPasswordToken"] = user.ResetToken; + + var resetPasswordUrl = _linkGenerator.GetUriByAction( + _hca.HttpContext, + "ResetPassword", + "ResetPassword", + new { area = "OrchardCore.Users", code = user.ResetToken }); + workflowContext.Properties["ResetPasswordUrl"] = resetPasswordUrl; + + return Outcomes("Done", "Done"); + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs new file mode 100644 index 00000000..fa705b4b --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs @@ -0,0 +1,11 @@ +using Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; +using Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; +using OrchardCore.Workflows.Display; + +namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers; + +public class GenerateResetPasswordTokenTaskDisplayDriver : ActivityDisplayDriver< + GenerateResetPasswordTokenTask, + GenerateResetPasswordTokenTaskViewModel> +{ +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs new file mode 100644 index 00000000..f86a15fe --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs @@ -0,0 +1,14 @@ +using Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; +using Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Modules; +using OrchardCore.Workflows.Helpers; + +namespace Lombiq.HelpfulExtensions.Extensions.Workflows; + +[Feature(FeatureIds.Workflows)] +public class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) => + services.AddActivity(); +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs new file mode 100644 index 00000000..879b1dc4 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs @@ -0,0 +1,5 @@ +namespace Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; + +public class GenerateResetPasswordTokenTaskViewModel +{ +} diff --git a/Lombiq.HelpfulExtensions/FeatureIds.cs b/Lombiq.HelpfulExtensions/FeatureIds.cs index 0bd95b31..6417776d 100644 --- a/Lombiq.HelpfulExtensions/FeatureIds.cs +++ b/Lombiq.HelpfulExtensions/FeatureIds.cs @@ -13,4 +13,5 @@ public static class FeatureIds public const string Security = FeatureIdPrefix + nameof(Security); public const string TargetBlank = FeatureIdPrefix + nameof(TargetBlank); public const string SiteTexts = FeatureIdPrefix + nameof(SiteTexts); + public const string Workflows = FeatureIdPrefix + nameof(Workflows); } diff --git a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj index 3231dfdd..cb9b9d6b 100644 --- a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj +++ b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj @@ -41,6 +41,7 @@ + diff --git a/Lombiq.HelpfulExtensions/Manifest.cs b/Lombiq.HelpfulExtensions/Manifest.cs index 71589119..4e9a32b6 100644 --- a/Lombiq.HelpfulExtensions/Manifest.cs +++ b/Lombiq.HelpfulExtensions/Manifest.cs @@ -101,3 +101,14 @@ "OrchardCore.Markdown", } )] + +[assembly: Feature( + Id = Workflows, + Name = "Lombiq Helpful Extensions - Workflows", + Category = "Workflows", + Description = "Adds useful workflow activities (e.g., generate reset password token).", + Dependencies = new[] + { + "OrchardCore.Workflows", + } +)] diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml new file mode 100644 index 00000000..c2b7eb05 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml @@ -0,0 +1,8 @@ +@using OrchardCore.Workflows.Helpers +@model OrchardCore.Workflows.ViewModels.ActivityViewModel + +
+

+ @Model.Activity.GetTitleOrDefault(() => T["Generate Reset Password Token"]) +

+
diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml new file mode 100644 index 00000000..e69de29b diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Thumbnail.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Thumbnail.cshtml new file mode 100644 index 00000000..08647469 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Thumbnail.cshtml @@ -0,0 +1,2 @@ +

@T["Generate Reset Password Token"]

+

@T["Generates a reset password token for the user in the workflow context or the currently logged in user and puts it into the Workflow context."]

diff --git a/Readme.md b/Readme.md index d35b5ea0..3335d7ac 100644 --- a/Readme.md +++ b/Readme.md @@ -102,6 +102,10 @@ Use the `ShellScope.Current.SendEmailDeferred()` for sending emails. It'll send Gives all external links the `target="_blank"` attribute. +### Workflows + +Adds useful Workflows activities such as the `GenerateResetPasswordTask` that can be used to generate a reset password token to a Workflow context~~~~. + ## Contributing and support Bug reports, feature requests, comments, questions, code contributions and love letters are warmly welcome. You can send them to us via GitHub issues and pull requests. Please adhere to our [open-source guidelines](https://lombiq.com/open-source-guidelines) while doing so. From 7115d546557f29f75e96407a3576ed7b11b99f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartha?= Date: Wed, 15 Feb 2023 18:09:39 +0100 Subject: [PATCH 2/6] Adjusting Readme --- Readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 3335d7ac..f5a94238 100644 --- a/Readme.md +++ b/Readme.md @@ -104,7 +104,9 @@ Gives all external links the `target="_blank"` attribute. ### Workflows -Adds useful Workflows activities such as the `GenerateResetPasswordTask` that can be used to generate a reset password token to a Workflow context~~~~. +Adds useful Workflows activities. + +- GenerateResetPasswordTask: Generates a reset password token for the user found in the Workflow context or the current user. The token is set to the Workflow properties to the `ResetPasswordToken` key. It also generates a URL for the built-in reset password page; it'll be set to the `ResetPasswordUrl` key. If you want to use the URL, make sure you have the Reset Password feature enabled and you have allowed users to reset their password by going to Admin UI > Security > Settings > Reset Password > tick "Allow the users to reset their password". ## Contributing and support From 47023339a0341db18c3d73eaeb9708145b556f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartha?= Date: Wed, 15 Feb 2023 18:24:09 +0100 Subject: [PATCH 3/6] Using named parameter to overcome analyzer violation --- .../Workflows/Activities/GenerateResetPasswordTokenTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs index 67eae847..e1019021 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs @@ -44,7 +44,7 @@ public GenerateResetPasswordTokenTask( public override IEnumerable GetPossibleOutcomes( WorkflowExecutionContext workflowContext, ActivityContext activityContext) => - Outcomes(T["Done"]); + Outcomes(names: T["Done"]); public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, From 0306aae0a5a50a4ad232ff6be552963887416087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartha?= Date: Mon, 27 Feb 2023 16:33:54 +0100 Subject: [PATCH 4/6] Making keys configurable and updating feature ID --- .../GenerateResetPasswordTokenTask.cs | 70 ++++++++++++++----- ...rateResetPasswordTokenTaskDisplayDriver.cs | 39 +++++++++++ .../Extensions/Workflows/Startup.cs | 2 +- ...GenerateResetPasswordTokenTaskViewModel.cs | 3 + Lombiq.HelpfulExtensions/FeatureIds.cs | 1 + Lombiq.HelpfulExtensions/Manifest.cs | 9 +-- ...eResetPasswordTokenTask.Fields.Edit.cshtml | 20 ++++++ Readme.md | 6 +- 8 files changed, 125 insertions(+), 25 deletions(-) diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs index e1019021..9762bde4 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs @@ -8,6 +8,7 @@ using OrchardCore.Workflows.Abstractions.Models; using OrchardCore.Workflows.Activities; using OrchardCore.Workflows.Models; +using OrchardCore.Workflows.Services; using System; using System.Collections.Generic; using System.Text; @@ -22,53 +23,90 @@ public class GenerateResetPasswordTokenTask : TaskActivity private readonly IHttpContextAccessor _hca; private readonly UserManager _userManager; private readonly IUserService _userService; + private readonly IWorkflowExpressionEvaluator _expressionEvaluator; public override string Name => nameof(GenerateResetPasswordTokenTask); public override LocalizedString DisplayText => T["Generate reset password token"]; public override LocalizedString Category => T["User"]; + public WorkflowExpression UserPropertyKey + { + get => GetProperty(() => new WorkflowExpression()); + set => SetProperty(value); + } + + public WorkflowExpression ResetPasswordTokenPropertyKey + { + get => GetProperty(() => new WorkflowExpression()); + set => SetProperty(value); + } + + public WorkflowExpression ResetPasswordUrlPropertyKey + { + get => GetProperty(() => new WorkflowExpression()); + set => SetProperty(value); + } + public GenerateResetPasswordTokenTask( IStringLocalizer localizer, LinkGenerator linkGenerator, IHttpContextAccessor hca, UserManager userManager, - IUserService userService) + IUserService userService, + IWorkflowExpressionEvaluator expressionEvaluator) { T = localizer; _linkGenerator = linkGenerator; _hca = hca; _userManager = userManager; _userService = userService; + _expressionEvaluator = expressionEvaluator; } public override IEnumerable GetPossibleOutcomes( WorkflowExecutionContext workflowContext, ActivityContext activityContext) => - Outcomes(names: T["Done"]); + Outcomes(T["Done"], T["Error"]); public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, ActivityContext activityContext) { - var user = workflowContext.Input["User"] as User ?? workflowContext.Properties["User"] as User; - if (user == null && _hca.HttpContext.User.Identity.IsAuthenticated) - { - user = await _userService.GetAuthenticatedUserAsync(_hca.HttpContext.User) as User; - } + var userPropertyKey = !string.IsNullOrEmpty(UserPropertyKey.Expression) + ? await _expressionEvaluator.EvaluateAsync(UserPropertyKey, workflowContext, encoder: null) + : null; + + var user = string.IsNullOrEmpty(userPropertyKey) + ? workflowContext.Input.GetMaybe("User") as User + : workflowContext.Properties.GetMaybe(userPropertyKey) as User ?? + await _userService.GetUserByUniqueIdAsync(workflowContext.Properties.GetMaybe(userPropertyKey) as string) as User; + + if (user == null) return Outcomes("Error"); - if (user == null) return Outcomes("Done", "Done"); + var resetPasswordTokenPropertyKey = !string.IsNullOrEmpty(ResetPasswordTokenPropertyKey.Expression) + ? await _expressionEvaluator.EvaluateAsync(ResetPasswordTokenPropertyKey, workflowContext, encoder: null) + : null; + var resetPasswordUrlPropertyKey = !string.IsNullOrEmpty(ResetPasswordUrlPropertyKey.Expression) + ? await _expressionEvaluator.EvaluateAsync(ResetPasswordUrlPropertyKey, workflowContext, encoder: null) + : null; var generatedToken = await _userManager.GeneratePasswordResetTokenAsync(user); user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(generatedToken)); - workflowContext.Properties["ResetPasswordToken"] = user.ResetToken; + if (!string.IsNullOrEmpty(resetPasswordTokenPropertyKey)) + { + workflowContext.Properties[resetPasswordTokenPropertyKey] = user.ResetToken; + } - var resetPasswordUrl = _linkGenerator.GetUriByAction( - _hca.HttpContext, - "ResetPassword", - "ResetPassword", - new { area = "OrchardCore.Users", code = user.ResetToken }); - workflowContext.Properties["ResetPasswordUrl"] = resetPasswordUrl; + if (!string.IsNullOrEmpty(resetPasswordUrlPropertyKey)) + { + var resetPasswordUrl = _linkGenerator.GetUriByAction( + _hca.HttpContext, + "ResetPassword", + "ResetPassword", + new { area = "OrchardCore.Users", code = user.ResetToken }); + workflowContext.Properties[resetPasswordUrlPropertyKey] = resetPasswordUrl; + } - return Outcomes("Done", "Done"); + return Outcomes("Done"); } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs index fa705b4b..06a818ff 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs @@ -1,6 +1,12 @@ using Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; using Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Mvc.ModelBinding; using OrchardCore.Workflows.Display; +using OrchardCore.Workflows.Models; +using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers; @@ -8,4 +14,37 @@ public class GenerateResetPasswordTokenTaskDisplayDriver : ActivityDisplayDriver GenerateResetPasswordTokenTask, GenerateResetPasswordTokenTaskViewModel> { + private readonly IStringLocalizer T; + + public GenerateResetPasswordTokenTaskDisplayDriver(IStringLocalizer localizer) => + T = localizer; + + protected override void EditActivity(GenerateResetPasswordTokenTask activity, GenerateResetPasswordTokenTaskViewModel model) + { + model.UserPropertyKey = activity.UserPropertyKey.Expression; + model.ResetPasswordTokenPropertyKey = activity.ResetPasswordTokenPropertyKey.Expression; + model.ResetPasswordUrlPropertyKey = activity.ResetPasswordUrlPropertyKey.Expression; + } + + public override async Task UpdateAsync(GenerateResetPasswordTokenTask model, IUpdateModel updater) + { + var viewModel = new GenerateResetPasswordTokenTaskViewModel(); + if (await updater.TryUpdateModelAsync(viewModel, Prefix)) + { + model.UserPropertyKey = new WorkflowExpression(viewModel.UserPropertyKey); + model.ResetPasswordTokenPropertyKey = new WorkflowExpression(viewModel.ResetPasswordTokenPropertyKey); + model.ResetPasswordUrlPropertyKey = new WorkflowExpression(viewModel.ResetPasswordUrlPropertyKey); + + if (string.IsNullOrEmpty(viewModel.ResetPasswordTokenPropertyKey) && + string.IsNullOrEmpty(viewModel.ResetPasswordUrlPropertyKey)) + { + updater.ModelState.AddModelError( + Prefix, + "TokenOrUrlIsRequired", + T["A value is required for either the token or the URL property key."]); + } + } + + return Edit(model); + } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs index f86a15fe..ade9027a 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Workflows; -[Feature(FeatureIds.Workflows)] +[Feature(FeatureIds.ResetPasswordActivity)] public class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) => diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs index 879b1dc4..24a7da14 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs @@ -2,4 +2,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; public class GenerateResetPasswordTokenTaskViewModel { + public string UserPropertyKey { get; set; } + public string ResetPasswordTokenPropertyKey { get; set; } + public string ResetPasswordUrlPropertyKey { get; set; } } diff --git a/Lombiq.HelpfulExtensions/FeatureIds.cs b/Lombiq.HelpfulExtensions/FeatureIds.cs index 6417776d..4255aa69 100644 --- a/Lombiq.HelpfulExtensions/FeatureIds.cs +++ b/Lombiq.HelpfulExtensions/FeatureIds.cs @@ -14,4 +14,5 @@ public static class FeatureIds public const string TargetBlank = FeatureIdPrefix + nameof(TargetBlank); public const string SiteTexts = FeatureIdPrefix + nameof(SiteTexts); public const string Workflows = FeatureIdPrefix + nameof(Workflows); + public const string ResetPasswordActivity = Workflows + "." + nameof(ResetPasswordActivity); } diff --git a/Lombiq.HelpfulExtensions/Manifest.cs b/Lombiq.HelpfulExtensions/Manifest.cs index 4e9a32b6..83018e3c 100644 --- a/Lombiq.HelpfulExtensions/Manifest.cs +++ b/Lombiq.HelpfulExtensions/Manifest.cs @@ -103,12 +103,13 @@ )] [assembly: Feature( - Id = Workflows, - Name = "Lombiq Helpful Extensions - Workflows", - Category = "Workflows", - Description = "Adds useful workflow activities (e.g., generate reset password token).", + Id = ResetPasswordActivity, + Name = "Lombiq Helpful Extensions - Reset password workflow activity", + Category = "Security", + Description = "Adds generate reset password token activity.", Dependencies = new[] { + "OrchardCore.Users.ResetPassword", "OrchardCore.Workflows", } )] diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml index e69de29b..97b70c96 100644 --- a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml +++ b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml @@ -0,0 +1,20 @@ +@model Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels.GenerateResetPasswordTokenTaskViewModel + +
+ + + + @T["Key of the property in the Workflow context dictionary that contains the user's ID or the User object. If it's empty, it'll try to find the User object in the Workflow input. With Liquid support."] +
+
+ + + + @T["Key of the property in the Workflow context where this Task will set the reset password token to. Optionally, leave it empty and set the URL field. With Liquid support."] +
+
+ + + + @T["Key of the property in the Workflow context where this Task will set the reset password URL to. Optionally, leave it empty and set the token field. With Liquid support."] +
diff --git a/Readme.md b/Readme.md index f5a94238..88e0d88e 100644 --- a/Readme.md +++ b/Readme.md @@ -102,11 +102,9 @@ Use the `ShellScope.Current.SendEmailDeferred()` for sending emails. It'll send Gives all external links the `target="_blank"` attribute. -### Workflows +### Reset Password activity -Adds useful Workflows activities. - -- GenerateResetPasswordTask: Generates a reset password token for the user found in the Workflow context or the current user. The token is set to the Workflow properties to the `ResetPasswordToken` key. It also generates a URL for the built-in reset password page; it'll be set to the `ResetPasswordUrl` key. If you want to use the URL, make sure you have the Reset Password feature enabled and you have allowed users to reset their password by going to Admin UI > Security > Settings > Reset Password > tick "Allow the users to reset their password". +Adds a workflow activity that generates a reset password token for the specified user. You can define the source of the User object or ID using activity parameters. It will set the token or the URL to the workflow context to a key that you define as an activity parameter. ## Contributing and support From a87e941683d82e0f561d2f4118053e96db092786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Bartha?= Date: Tue, 28 Feb 2023 16:31:48 +0100 Subject: [PATCH 5/6] Retrieving User object using expression --- .../GenerateResetPasswordTokenTask.cs | 64 ++++++++----------- ...rateResetPasswordTokenTaskDisplayDriver.cs | 23 ++----- .../GenerateResetPasswordTokenResult.cs | 7 ++ .../Extensions/Workflows/Startup.cs | 8 ++- ...GenerateResetPasswordTokenTaskViewModel.cs | 5 +- ...esetPasswordTokenTask.Fields.Design.cshtml | 2 + ...eResetPasswordTokenTask.Fields.Edit.cshtml | 18 +++--- Readme.md | 2 +- 8 files changed, 65 insertions(+), 64 deletions(-) create mode 100644 Lombiq.HelpfulExtensions/Extensions/Workflows/Models/GenerateResetPasswordTokenResult.cs diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs index 9762bde4..5941ed81 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs @@ -1,10 +1,10 @@ +using Lombiq.HelpfulExtensions.Extensions.Workflows.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; using OrchardCore.Users; using OrchardCore.Users.Models; -using OrchardCore.Users.Services; using OrchardCore.Workflows.Abstractions.Models; using OrchardCore.Workflows.Activities; using OrchardCore.Workflows.Models; @@ -22,28 +22,27 @@ public class GenerateResetPasswordTokenTask : TaskActivity private readonly LinkGenerator _linkGenerator; private readonly IHttpContextAccessor _hca; private readonly UserManager _userManager; - private readonly IUserService _userService; - private readonly IWorkflowExpressionEvaluator _expressionEvaluator; + private readonly IWorkflowScriptEvaluator _workflowScriptEvaluator; public override string Name => nameof(GenerateResetPasswordTokenTask); public override LocalizedString DisplayText => T["Generate reset password token"]; public override LocalizedString Category => T["User"]; - public WorkflowExpression UserPropertyKey + public WorkflowExpression User { - get => GetProperty(() => new WorkflowExpression()); + get => GetProperty(() => new WorkflowExpression()); set => SetProperty(value); } - public WorkflowExpression ResetPasswordTokenPropertyKey + public string ResetPasswordTokenPropertyKey { - get => GetProperty(() => new WorkflowExpression()); + get => GetProperty(); set => SetProperty(value); } - public WorkflowExpression ResetPasswordUrlPropertyKey + public string ResetPasswordUrlPropertyKey { - get => GetProperty(() => new WorkflowExpression()); + get => GetProperty(); set => SetProperty(value); } @@ -52,15 +51,13 @@ public GenerateResetPasswordTokenTask( LinkGenerator linkGenerator, IHttpContextAccessor hca, UserManager userManager, - IUserService userService, - IWorkflowExpressionEvaluator expressionEvaluator) + IWorkflowScriptEvaluator workflowScriptEvaluator) { T = localizer; _linkGenerator = linkGenerator; _hca = hca; _userManager = userManager; - _userService = userService; - _expressionEvaluator = expressionEvaluator; + _workflowScriptEvaluator = workflowScriptEvaluator; } public override IEnumerable GetPossibleOutcomes( @@ -72,39 +69,34 @@ public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, ActivityContext activityContext) { - var userPropertyKey = !string.IsNullOrEmpty(UserPropertyKey.Expression) - ? await _expressionEvaluator.EvaluateAsync(UserPropertyKey, workflowContext, encoder: null) + var user = !string.IsNullOrEmpty(User.Expression) + ? await _workflowScriptEvaluator.EvaluateAsync(User, workflowContext) : null; - var user = string.IsNullOrEmpty(userPropertyKey) - ? workflowContext.Input.GetMaybe("User") as User - : workflowContext.Properties.GetMaybe(userPropertyKey) as User ?? - await _userService.GetUserByUniqueIdAsync(workflowContext.Properties.GetMaybe(userPropertyKey) as string) as User; - if (user == null) return Outcomes("Error"); - var resetPasswordTokenPropertyKey = !string.IsNullOrEmpty(ResetPasswordTokenPropertyKey.Expression) - ? await _expressionEvaluator.EvaluateAsync(ResetPasswordTokenPropertyKey, workflowContext, encoder: null) - : null; - var resetPasswordUrlPropertyKey = !string.IsNullOrEmpty(ResetPasswordUrlPropertyKey.Expression) - ? await _expressionEvaluator.EvaluateAsync(ResetPasswordUrlPropertyKey, workflowContext, encoder: null) - : null; - var generatedToken = await _userManager.GeneratePasswordResetTokenAsync(user); user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(generatedToken)); - if (!string.IsNullOrEmpty(resetPasswordTokenPropertyKey)) + var resetPasswordUrl = _linkGenerator.GetUriByAction( + _hca.HttpContext, + "ResetPassword", + "ResetPassword", + new { area = "OrchardCore.Users", code = user.ResetToken }); + + workflowContext.LastResult = new GenerateResetPasswordTokenResult + { + ResetPasswordToken = user.ResetToken, + ResetPasswordUrl = resetPasswordUrl, + }; + + if (!string.IsNullOrEmpty(ResetPasswordTokenPropertyKey)) { - workflowContext.Properties[resetPasswordTokenPropertyKey] = user.ResetToken; + workflowContext.Properties[ResetPasswordTokenPropertyKey] = user.ResetToken; } - if (!string.IsNullOrEmpty(resetPasswordUrlPropertyKey)) + if (!string.IsNullOrEmpty(ResetPasswordUrlPropertyKey)) { - var resetPasswordUrl = _linkGenerator.GetUriByAction( - _hca.HttpContext, - "ResetPassword", - "ResetPassword", - new { area = "OrchardCore.Users", code = user.ResetToken }); - workflowContext.Properties[resetPasswordUrlPropertyKey] = resetPasswordUrl; + workflowContext.Properties[ResetPasswordUrlPropertyKey] = resetPasswordUrl; } return Outcomes("Done"); diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs index 06a818ff..32be3ce2 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Localization; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Mvc.ModelBinding; +using OrchardCore.Users.Models; using OrchardCore.Workflows.Display; using OrchardCore.Workflows.Models; using System.Threading.Tasks; @@ -21,9 +21,9 @@ public GenerateResetPasswordTokenTaskDisplayDriver(IStringLocalizer UpdateAsync(GenerateResetPasswordTokenTask model, IUpdateModel updater) @@ -31,18 +31,9 @@ public override async Task UpdateAsync(GenerateResetPasswordToke var viewModel = new GenerateResetPasswordTokenTaskViewModel(); if (await updater.TryUpdateModelAsync(viewModel, Prefix)) { - model.UserPropertyKey = new WorkflowExpression(viewModel.UserPropertyKey); - model.ResetPasswordTokenPropertyKey = new WorkflowExpression(viewModel.ResetPasswordTokenPropertyKey); - model.ResetPasswordUrlPropertyKey = new WorkflowExpression(viewModel.ResetPasswordUrlPropertyKey); - - if (string.IsNullOrEmpty(viewModel.ResetPasswordTokenPropertyKey) && - string.IsNullOrEmpty(viewModel.ResetPasswordUrlPropertyKey)) - { - updater.ModelState.AddModelError( - Prefix, - "TokenOrUrlIsRequired", - T["A value is required for either the token or the URL property key."]); - } + model.User = new WorkflowExpression(viewModel.UserExpression); + model.ResetPasswordTokenPropertyKey = viewModel.ResetPasswordTokenPropertyKey; + model.ResetPasswordUrlPropertyKey = viewModel.ResetPasswordUrlPropertyKey; } return Edit(model); diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Models/GenerateResetPasswordTokenResult.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Models/GenerateResetPasswordTokenResult.cs new file mode 100644 index 00000000..ec79ffaa --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Models/GenerateResetPasswordTokenResult.cs @@ -0,0 +1,7 @@ +namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Models; + +public class GenerateResetPasswordTokenResult +{ + public string ResetPasswordToken { get; set; } + public string ResetPasswordUrl { get; set; } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs index ade9027a..0a71351e 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs @@ -1,5 +1,7 @@ +using Fluid; using Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; using Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers; +using Lombiq.HelpfulExtensions.Extensions.Workflows.Models; using Microsoft.Extensions.DependencyInjection; using OrchardCore.Modules; using OrchardCore.Workflows.Helpers; @@ -9,6 +11,10 @@ namespace Lombiq.HelpfulExtensions.Extensions.Workflows; [Feature(FeatureIds.ResetPasswordActivity)] public class Startup : StartupBase { - public override void ConfigureServices(IServiceCollection services) => + public override void ConfigureServices(IServiceCollection services) + { services.AddActivity(); + services.Configure(option => + option.MemberAccessStrategy.Register()); + } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs index 24a7da14..9e3f6d22 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/ViewModels/GenerateResetPasswordTokenTaskViewModel.cs @@ -1,8 +1,11 @@ +using System.ComponentModel.DataAnnotations; + namespace Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; public class GenerateResetPasswordTokenTaskViewModel { - public string UserPropertyKey { get; set; } + [Required] + public string UserExpression { get; set; } public string ResetPasswordTokenPropertyKey { get; set; } public string ResetPasswordUrlPropertyKey { get; set; } } diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml index c2b7eb05..bf6f6c2d 100644 --- a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml +++ b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Design.cshtml @@ -6,3 +6,5 @@ @Model.Activity.GetTitleOrDefault(() => T["Generate Reset Password Token"]) + +@T["User expression: {0}", Model.Activity.User?.Expression] diff --git a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml index 97b70c96..862a0857 100644 --- a/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml +++ b/Lombiq.HelpfulExtensions/Views/Items/GenerateResetPasswordTokenTask.Fields.Edit.cshtml @@ -1,20 +1,20 @@ @model Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels.GenerateResetPasswordTokenTaskViewModel -
- - - - @T["Key of the property in the Workflow context dictionary that contains the user's ID or the User object. If it's empty, it'll try to find the User object in the Workflow input. With Liquid support."] +
+ + + + @T["Enter a JavaScript expression that evaluates to the User object."]
- + - @T["Key of the property in the Workflow context where this Task will set the reset password token to. Optionally, leave it empty and set the URL field. With Liquid support."] + @T["Optional key of the property in the Workflow context where this Task will set the reset password token to."]
- + - @T["Key of the property in the Workflow context where this Task will set the reset password URL to. Optionally, leave it empty and set the token field. With Liquid support."] + @T["Optional key of the property in the Workflow context where this Task will set the reset password URL to."]
diff --git a/Readme.md b/Readme.md index 6b3eba4b..bef12179 100644 --- a/Readme.md +++ b/Readme.md @@ -127,7 +127,7 @@ Gives all external links the `target="_blank"` attribute. ### Reset Password activity -Adds a workflow activity that generates a reset password token for the specified user. You can define the source of the User object or ID using activity parameters. It will set the token or the URL to the workflow context to a key that you define as an activity parameter. +Adds a workflow activity that generates a reset password token for the specified user. You can define the source of the User object using a JavaScript expression. It will set the token and the URL to the workflow `LastResult` property and optionally it can set them to the `Properties` dictionary to a key that you define as an activity parameter. ## Contributing and support From a7b488d1e85dbfc62dbfffcb8fcde0f0790f5495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3zsef=20Horv=C3=A1th?= Date: Tue, 28 Feb 2023 17:28:02 +0100 Subject: [PATCH 6/6] Removin unnecessary checking --- .../Workflows/Activities/GenerateResetPasswordTokenTask.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs index 5941ed81..a1181e43 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Activities/GenerateResetPasswordTokenTask.cs @@ -69,9 +69,7 @@ public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, ActivityContext activityContext) { - var user = !string.IsNullOrEmpty(User.Expression) - ? await _workflowScriptEvaluator.EvaluateAsync(User, workflowContext) - : null; + var user = await _workflowScriptEvaluator.EvaluateAsync(User, workflowContext); if (user == null) return Outcomes("Error");