Skip to content
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

OSOE-572: Add "Generate Reset Password Token" workflow activity #113

Merged
merged 8 commits into from
Feb 28, 2023
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
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);
dministro marked this conversation as resolved.
Show resolved Hide resolved
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