Skip to content

Commit

Permalink
Merge pull request #113 from Lombiq/issue/OSOE-572
Browse files Browse the repository at this point in the history
OSOE-572: Add "Generate Reset Password Token" workflow activity
  • Loading branch information
dministro authored Feb 28, 2023
2 parents cfd6983 + a7b488d commit 19cc2d4
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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.Workflows.Abstractions.Models;
using OrchardCore.Workflows.Activities;
using OrchardCore.Workflows.Models;
using OrchardCore.Workflows.Services;
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<GenerateResetPasswordTokenTask> T;
private readonly LinkGenerator _linkGenerator;
private readonly IHttpContextAccessor _hca;
private readonly UserManager<IUser> _userManager;
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<User> User
{
get => GetProperty(() => new WorkflowExpression<User>());
set => SetProperty(value);
}

public string ResetPasswordTokenPropertyKey
{
get => GetProperty<string>();
set => SetProperty(value);
}

public string ResetPasswordUrlPropertyKey
{
get => GetProperty<string>();
set => SetProperty(value);
}

public GenerateResetPasswordTokenTask(
IStringLocalizer<GenerateResetPasswordTokenTask> localizer,
LinkGenerator linkGenerator,
IHttpContextAccessor hca,
UserManager<IUser> userManager,
IWorkflowScriptEvaluator workflowScriptEvaluator)
{
T = localizer;
_linkGenerator = linkGenerator;
_hca = hca;
_userManager = userManager;
_workflowScriptEvaluator = workflowScriptEvaluator;
}

public override IEnumerable<Outcome> GetPossibleOutcomes(
WorkflowExecutionContext workflowContext,
ActivityContext activityContext) =>
Outcomes(T["Done"], T["Error"]);

public override async Task<ActivityExecutionResult> ExecuteAsync(
WorkflowExecutionContext workflowContext,
ActivityContext activityContext)
{
var user = await _workflowScriptEvaluator.EvaluateAsync(User, workflowContext);

if (user == null) return Outcomes("Error");

var generatedToken = await _userManager.GeneratePasswordResetTokenAsync(user);
user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(generatedToken));
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;
}

if (!string.IsNullOrEmpty(ResetPasswordUrlPropertyKey))
{
workflowContext.Properties[ResetPasswordUrlPropertyKey] = resetPasswordUrl;
}

return Outcomes("Done");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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.Users.Models;
using OrchardCore.Workflows.Display;
using OrchardCore.Workflows.Models;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers;

public class GenerateResetPasswordTokenTaskDisplayDriver : ActivityDisplayDriver<
GenerateResetPasswordTokenTask,
GenerateResetPasswordTokenTaskViewModel>
{
private readonly IStringLocalizer T;

public GenerateResetPasswordTokenTaskDisplayDriver(IStringLocalizer<GenerateResetPasswordTokenTaskDisplayDriver> localizer) =>
T = localizer;

protected override void EditActivity(GenerateResetPasswordTokenTask activity, GenerateResetPasswordTokenTaskViewModel model)
{
model.UserExpression = activity.User.Expression;
model.ResetPasswordTokenPropertyKey = activity.ResetPasswordTokenPropertyKey;
model.ResetPasswordUrlPropertyKey = activity.ResetPasswordUrlPropertyKey;
}

public override async Task<IDisplayResult> UpdateAsync(GenerateResetPasswordTokenTask model, IUpdateModel updater)
{
var viewModel = new GenerateResetPasswordTokenTaskViewModel();
if (await updater.TryUpdateModelAsync(viewModel, Prefix))
{
model.User = new WorkflowExpression<User>(viewModel.UserExpression);
model.ResetPasswordTokenPropertyKey = viewModel.ResetPasswordTokenPropertyKey;
model.ResetPasswordUrlPropertyKey = viewModel.ResetPasswordUrlPropertyKey;
}

return Edit(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Models;

public class GenerateResetPasswordTokenResult
{
public string ResetPasswordToken { get; set; }
public string ResetPasswordUrl { get; set; }
}
20 changes: 20 additions & 0 deletions Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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;

namespace Lombiq.HelpfulExtensions.Extensions.Workflows;

[Feature(FeatureIds.ResetPasswordActivity)]
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddActivity<GenerateResetPasswordTokenTask, GenerateResetPasswordTokenTaskDisplayDriver>();
services.Configure<TemplateOptions>(option =>
option.MemberAccessStrategy.Register<GenerateResetPasswordTokenResult>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels;

public class GenerateResetPasswordTokenTaskViewModel
{
[Required]
public string UserExpression { get; set; }
public string ResetPasswordTokenPropertyKey { get; set; }
public string ResetPasswordUrlPropertyKey { get; set; }
}
2 changes: 2 additions & 0 deletions Lombiq.HelpfulExtensions/FeatureIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public static class FeatureIds
public const string TargetBlank = FeatureIdPrefix + nameof(TargetBlank);
public const string SiteTexts = FeatureIdPrefix + nameof(SiteTexts);
public const string OrchardRecipeMigration = FeatureIdPrefix + nameof(OrchardRecipeMigration);
public const string Workflows = FeatureIdPrefix + nameof(Workflows);
public const string ResetPasswordActivity = Workflows + "." + nameof(ResetPasswordActivity);
}
1 change: 1 addition & 0 deletions Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<PackageReference Include="OrchardCore.Markdown" Version="1.5.0" />
<PackageReference Include="OrchardCore.MetaWeblog.Abstractions" Version="1.5.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.5.0" />
<PackageReference Include="OrchardCore.Workflows" Version="1.5.0" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
Expand Down
12 changes: 12 additions & 0 deletions Lombiq.HelpfulExtensions/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,15 @@
"CommonPart and BodyPart (full list is in the Helpful Extensions repository readme), but can be extended " +
"with additional converters that only have to handle more specialized export data."
)]

[assembly: Feature(
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",
}
)]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@using OrchardCore.Workflows.Helpers
@model OrchardCore.Workflows.ViewModels.ActivityViewModel<Lombiq.HelpfulExtensions.Extensions.Workflows.Activities.GenerateResetPasswordTokenTask>

<header>
<h4>
<i class="fa fa-user" aria-hidden="true"></i>@Model.Activity.GetTitleOrDefault(() => T["Generate Reset Password Token"])
</h4>
</header>

<span>@T["User expression: <em>{0}</em>", Model.Activity.User?.Expression]</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@model Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels.GenerateResetPasswordTokenTaskViewModel

<div class="mb-3" asp-validation-class-for="UserExpression">
<label asp-for="UserExpression">@T["User Expression"]</label>
<input type="text" asp-for="UserExpression" class="form-control code" />
<span asp-validation-for="UserExpression"></span>
<span class="hint">@T["Enter a JavaScript expression that evaluates to the User object."]</span>
</div>
<div class="mb-3" asp-validation-class-for="ResetPasswordTokenPropertyKey">
<label asp-for="ResetPasswordTokenPropertyKey">@T["Reset Password Token Property Key"]</label>
<input type="text" asp-for="ResetPasswordTokenPropertyKey" class="form-control" />
<span asp-validation-for="ResetPasswordTokenPropertyKey"></span>
<span class="hint">@T["Optional key of the property in the Workflow context where this Task will set the reset password token to."]</span>
</div>
<div class="mb-3" asp-validation-class-for="ResetPasswordUrlPropertyKey">
<label asp-for="ResetPasswordUrlPropertyKey">@T["Reset Password URL Property Key"]</label>
<input type="text" asp-for="ResetPasswordUrlPropertyKey" class="form-control" />
<span asp-validation-for="ResetPasswordUrlPropertyKey"></span>
<span class="hint">@T["Optional key of the property in the Workflow context where this Task will set the reset password URL to."]</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h4 class="card-title"><i class="fa fa-user" aria-hidden="true"></i>@T["Generate Reset Password Token"]</h4>
<p>@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."]</p>
4 changes: 4 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Use the `ShellScope.Current.SendEmailDeferred()` for sending emails. It'll send

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 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

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.
Expand Down

0 comments on commit 19cc2d4

Please sign in to comment.