Orchard work flow - Send email to multiple admin and moderator on content post/Update - workflow

I am working with Orchard CMS.
I have created work flow which send mail to Super admin and user himself who create block/news as a notification.
In my CMS there more than one admin and moderator. I want to send email to all moderator and admin as soon as new blog /news/article is posted or updated. Right now it is sending to only one admin user not all.
How can I create workflow to send mail to all admin as well as moderator once content (news/blog) is posted or updated?
I am working on orchard version 1.10.0.

There are two possibilities here: A custom workflow task and a simple token.
Let's start with the task. I've created a simple demo; here's the code:
Activities/RoleMailerTask.cs
namespace My.Module
{
using System.Collections.Generic;
using System.Linq;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Email.Activities;
using Orchard.Email.Services;
using Orchard.Environment.Extensions;
using Orchard.Localization;
using Orchard.Messaging.Services;
using Orchard.Roles.Models;
using Orchard.Roles.Services;
using Orchard.Users.Models;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
[OrchardFeature(Statics.FeatureNames.RoleMailerTask)]
public class RoleMailerTask : Task
{
private readonly IOrchardServices services;
public const string TaskName = "RoleMailer";
public RoleMailerTask(IOrchardServices services)
{
this.services = services;
this.T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public override string Name
{
get { return TaskName; }
}
public override string Form
{
get { return TaskName; }
}
public override LocalizedString Category
{
get { return this.T("Messaging"); }
}
public override LocalizedString Description
{
get { return this.T("Sends mails to all users with this chosen role"); }
}
public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
{
yield return this.T("Done");
}
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var recipientsRole = int.Parse(activityContext.GetState<string>("RecipientsRole"));
var role = this.services.WorkContext.Resolve<IRoleService>().GetRole(recipientsRole);
var userRolesRepo = this.services.WorkContext.Resolve<IRepository<UserRolesPartRecord>>();
var recepientIds = userRolesRepo.Table.Where(x => x.Role.Name == role.Name).Select(x => x.UserId);
var recipients = this.services.ContentManager.GetMany<UserPart>(recepientIds, VersionOptions.Published, QueryHints.Empty)
.Where(x => !string.IsNullOrEmpty(x.Email))
.Select(x => x.Email);
var body = activityContext.GetState<string>("Body");
var subject = activityContext.GetState<string>("Subject");
var replyTo = activityContext.GetState<string>("ReplyTo");
var bcc = activityContext.GetState<string>("Bcc");
var cc = activityContext.GetState<string>("CC");
var parameters = new Dictionary<string, object> {
{"Subject", subject},
{"Body", body},
{"Recipients", string.Join(",", recipients)},
{"ReplyTo", replyTo},
{"Bcc", bcc},
{"CC", cc}
};
var queued = activityContext.GetState<bool>("Queued");
if ( !queued )
{
this.services.WorkContext.Resolve<IMessageService>().Send(SmtpMessageChannel.MessageType, parameters);
}
else
{
var priority = activityContext.GetState<int>("Priority");
this.services.WorkContext.Resolve<IJobsQueueService>().Enqueue("IMessageService.Send", new { type = SmtpMessageChannel.MessageType, parameters = parameters }, priority);
}
yield return T("Done");
}
}
}
You might want to throw the recipients in a loop, and send a new email for each of them. With the version above, everyone will see the mail address of everyone else...
Forms/RoleMailerTaskForm.cs
namespace My.Module
{
using System;
using System.Linq;
using System.Web.Mvc;
using Activities;
using Orchard;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Environment.Extensions;
using Orchard.Forms.Services;
using Orchard.Roles.Services;
[OrchardFeature(Statics.FeatureNames.RoleMailerTask)]
public class RoleMailerTaskForm : Component, IFormProvider
{
private readonly IRoleService roleService;
public RoleMailerTaskForm(IShapeFactory shapeFactory, IRoleService roleService)
{
this.roleService = roleService;
this.Shape = shapeFactory;
}
public dynamic Shape { get; set; }
public void Describe(DescribeContext context)
{
Func<IShapeFactory, dynamic> formFactory = shape =>
{
var form = this.Shape.Form(
Id: RoleMailerTask.TaskName,
_Type: this.Shape.FieldSet(
Title: this.T("Send to"),
_RecepientsRole: this.Shape.SelectList(
Id: "recipientsRole",
Name: "RecipientsRole",
Title: this.T("Recepient role"),
Description: this.T( "The role of the users that should be notified" ),
Items: this.roleService.GetRoles().Select(r => new SelectListItem { Value = r.Id.ToString(), Text = r.Name })
),
_Bcc: this.Shape.TextBox(
Id: "bcc",
Name: "Bcc",
Title: this.T("Bcc"),
Description: this.T("Specify a comma-separated list of email addresses for a blind carbon copy"),
Classes: new[] { "large", "text", "tokenized" }),
_CC: this.Shape.TextBox(
Id: "cc",
Name: "CC",
Title: this.T("CC"),
Description: this.T("Specify a comma-separated list of email addresses for a carbon copy"),
Classes: new[] { "large", "text", "tokenized" }),
_ReplyTo: this.Shape.Textbox(
Id: "reply-to",
Name: "ReplyTo",
Title: this.T("Reply To Address"),
Description: this.T("If necessary, specify an email address for replies."),
Classes: new[] { "large", "text", "tokenized" }),
_Subject: this.Shape.Textbox(
Id: "Subject", Name: "Subject",
Title: this.T("Subject"),
Description: this.T("The subject of the email message."),
Classes: new[] { "large", "text", "tokenized" }),
_Message: this.Shape.Textarea(
Id: "Body", Name: "Body",
Title: this.T("Body"),
Description: this.T("The body of the email message."),
Classes: new[] { "tokenized" })
));
return form;
};
context.Form(RoleMailerTask.TaskName, formFactory);
}
}
This is the form you'll need to configure the task. The form is a blatant rip off from Orchard.Email/Forms/EmailForm. The only thing I've changed is the select list on top of the form.
And that's all you need!
For the token approach, you would just need to define a token like
{RoleRecpients:TheRoleYouWant}, parse that role name and get the users like shown in the Task above.

Related

WebApi with Axios

Goal:
Use Axios with put and post method from react TS to Backend WebApi c#.
Problem:
The code doesn't work in relation to CORS.
What part from the backend source code am I missing in order to make backend to retrieve data that is PUT or POST?
Thank you!
React TS
import React from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';
function App() {
const get = () => {
axios.get("https://localhost:7177/WeatherForecast/GetTestData")
.then(
response => console.log(response)
);
};
const update = () => {
const data =
{
CandyNumber: Number("1"),
Name: "asdf"
};
axios.put("https://localhost:7177/WeatherForecast/UpdateTestData", data)
.then(
response => console.log(response)
);
};
const add = () => {
const data =
{
CandyNumber: Number("1"),
content: "asdf"
};
axios.post("https://localhost:7177/WeatherForecast/AddTestData", data)
.then(
response => console.log(response)
);
};
return (
Get
Add
Uppdate
);
}
export default App;
backend
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace WebTest2.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet("GetTestData", Name = "GetTestData")]
[ProducesResponseType(typeof(List<Test>), StatusCodes.Status200OK)]
[ProducesResponseType(204)]
[Produces("application/json")]
public async Task<IActionResult> GetTestData()
{
List<Test> mytest = new List<Test>();
Test myTest = new Test()
{
CandyNumber = 1,
Name = "asdf"
};
Test myTest2 = new Test()
{
CandyNumber = 2,
Name = "asdf"
};
mytest.Add(myTest);
mytest.Add(myTest2);
return Ok(mytest);
}
[HttpPost("AddTestdata", Name = "AddTestdata")]
public async Task<IActionResult> AddTestdata(Test test)
{
List<Test> mytest = new List<Test>();
Test myTest = new Test()
{
CandyNumber = 1,
Name = "asdf"
};
Test myTest2 = new Test()
{
CandyNumber = 2,
Name = "asdf"
};
mytest.Add(myTest);
mytest.Add(myTest2);
return StatusCode(StatusCodes.Status204NoContent, null);
}
[HttpPut("UpdateTestdata", Name = "UpdateTestdata")]
public async Task<IActionResult> UpdateTestdata(Test test)
{
return StatusCode(StatusCodes.Status204NoContent, null);
}
}
public class Test
{
public int CandyNumber { get; set; }
public string Name { get; set; }
}
}
Program.cs
using Microsoft.OpenApi.Models;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = ""
});
});
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllHeaders",
corsbuilder =>
{
corsbuilder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins("http://localhost:3000/");
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//--
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Add
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true) // allow any origin
.AllowCredentials()); // allow credentials
after " app.UseSwaggerUI();"

Unknown argument error when creating record

I'm encountering some interesting behaviour when using Prisma ORM. It is related to Prisma's generated types, and I've been skimming the docs trying to find out more, but there doesn't seem to be much info about generated types in there (please correct me if I'm mistaken). Here's the behaviour:
Say I have a model with two 1-1 relations (Profile in the example below):
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int #id #default(autoincrement())
name String
profile Profile?
}
model Profile {
id Int #id #default(autoincrement())
name String
userId Int?
user User? #relation(fields: [userId], references: [id])
photoId Int?
photo Photo? #relation(fields: [photoId], references: [id])
}
model Photo {
id Int #id #default(autoincrement())
url String
profile Profile?
}
The following code works when creating a new profile:
const user = await prisma.user.create({ data: { name: "TestUser" } });
const profile = await prisma.profile.create({
data: {
name: "TestProfile",
user: { connect: { id: user.id } },
photo: { create: { url: "http://example.com/img" } },
},
});
... but this fails with an error:
const user = await prisma.user.create({ data: { name: "TestUser" } });
const profile = await prisma.profile.create({
data: {
name: "TestProfile",
userId: user.id,
photo: { create: { url: "http://example.com/img" } },
},
});
The error is:
Unknown arg userId in data.userId for type ProfileCreateInput. Did you mean user? Available args:
type ProfileCreateInput {
  name: String
  user?: UserCreateNestedOneWithoutProfileInput
  photo?: PhotoCreateNestedOneWithoutProfileInput
}
Why is the second create-profile code invalid?
Prisma essentially generates two type definitions for a create query. This is implemented with a XOR type, which ensures that only one definition out of two is fully specified and passed to the query:
export type ProfileCreateArgs = {
/* ... */
data: XOR<ProfileCreateInput, ProfileUncheckedCreateInput>;
}
The definitions are called checked and unchecked, the former using nested fields and the latter using raw ids:
export type ProfileCreateInput = {
id?: number;
/* ... */
user?: UserCreateNestedOneWithoutProfileInput;
photo?: PhotoCreateNestedOneWithoutProfileInput;
}
export type ProfileUncheckedCreateInput = {
id?: number;
/* ... */
userId?: number;
photoId?: number;
}
Which basically means that you either provide all references as connect, create etc. relations (checked) or as raw ids (unchecked). You cannot mix the styles, this is not supported.

In AspBoilerPlate - Unauthorized error when calling from Angular when Windows Authentication is On

Have already raised this before and thought I have addressed it as per what suggested on THIS and THIS but seems not!
I am using ABP template (Angular and ASP .NET CORE Application) on Full .Net Framework. I simply want to use Windows Authentication to Authenticate user.
I added [Authorize] to the Authenticate in the TokenAuthController and have finally got the HttpContext.User.Identity.Name populated but only when I call the Authenticate from the Swagger (http://localhost:21021/swagger). But I am getting Unauthorized error when calling the method from Angular (login.service.ts):
POST http://localhost:21021/api/TokenAuth/Authenticate 401 (Unauthorized)
Here is the steps I have taken so far:
Changed launchSetting.json:
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:21021/",
"sslPort": 0
}
},
Added ExternalAuthenticationSource:
public class WindowsAuthSource : DefaultExternalAuthenticationSource<Tenant, User>, ITransientDependency
{
public override string Name
{
get { return "Windows Authentication"; }
}
public override Task<bool> TryAuthenticateAsync(string userNameOrEmailAddress, string plainPassword, Tenant tenant)
{
return Task.FromResult(true);
}
}
Added it to CoreModule:
Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add<WindowsAuthSource>();
4.Adjust AuthConfigurer:
services.AddAuthentication(opt => {
opt.DefaultScheme = IISDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = IISDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
});
Adjust StartUp.cs:
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "WINDOWS";
iis.AutomaticAuthentication = true;
});
Changed Authenticate method in the TokenAuthController:
public async Task<AuthenticateResultModel> Authenticate([FromBody]
AuthenticateModel model)
{
//var username = WindowsIdentity.GetCurrent().Name.Split('\\').Last();
var username = HttpContext.User.Identity.Name;
model.UserNameOrEmailAddress = username;
var loginResult = await GetLoginResultAsync(
model.UserNameOrEmailAddress,
model.Password,
null
);
var accessToken = CreateAccessToken(CreateJwtClaims(loginResult.Identity));
return new AuthenticateResultModel
{
AccessToken = accessToken,
EncryptedAccessToken = GetEncrpyedAccessToken(accessToken),
ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds,
UserId = loginResult.User.Id
};
}
Sending dummy username and password from login.service.ts:
authenticate(finallyCallback?: () => void): void {
finallyCallback = finallyCallback || (() => { });
//Dummy data
this.authenticateModel.userNameOrEmailAddress = "DummyUsername";
this.authenticateModel.password = "DummyPassword";
this._tokenAuthService
.authenticate(this.authenticateModel)
.finally(finallyCallback)
.subscribe((result: AuthenticateResultModel) => {
this.processAuthenticateResult(result);
});
}

Unsupported Grant Type with CustomGrantValidator with IdentityServer 3

I'm trying to set up our IdentityServer solution to accept a custom Grant Validator. Our API project is accessed by to UIs, one that uses Password authentication (which is working) and now one that will use a 3rd party authentication.
In our API I've set up IdentityServer like so:
Startup.cs
public void Configuration(IAppBuilder app)
{
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var userService = new IdentityUserService();
factory.UserService = new Registration<IUserService>(resolver => userService);
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator, MyGrantValidator>());
var options = new IdentityServerOptions
{
SiteName = "My App Name",
SigningCertificate = Certificate.Get(),
Factory = factory
};
app.Map("/identity", identityServerApp =>
{
identityServerApp.UseIdentityServer(options);
});
}
MyGrantValidator.cs:
public class MyGrantValidator : ICustomGrantValidator
{
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
// For now I just want a basic response. More logic will come later.
var authResult = new AuthenticateResult(
subject: "1234", // user.AccountId.ToString(),
name: "bob" //context.UserName
);
var grantResult = new CustomGrantValidationResult
{
IsError = authResult.IsError,
Error = authResult.ErrorMessage,
ErrorDescription = authResult.ErrorMessage,
Principal = authResult.User
};
return await Task.FromResult(grantResult);
}
public string GrantType => "myGrantType";
}
In my UI, I setup a client like this:
var owinContext = HttpContext.GetOwinContext();
var token = owinContext.Authentication.User.FindFirst(c => c.Type == "myToken")?.Value;
var tokenId = owinContext.Authentication.User.FindFirst(c => c.Type == ClaimTypes.Sid)?.Value;
var client = new TokenClient(
ConfigurationManager.AppSettings["IdentityServerBaseUrl"] + "/connect/token",
"MyUser",
ConfigurationManager.AppSettings["MyClientSecret"],
AuthenticationStyle.Custom
);
var tokenResponse = client.RequestCustomGrantAsync(
"myGrantType",
"read write",
new Dictionary<string, string>
{
{ "token", token },
{ "tokenId", tokenId }
}
).Result;
return Redirect(returnUrl);
When the Request is triggered, I get: unsupported_grant_type
What am I missing?
You're using a client called "MyUser" (weird name for a client, but ok). Is that client registered as one of the in-memory clients with grant type set to "custom"?

ASP.NET MVC 3 - Custom SEO friendly routes

I've defined the following route:
routes.MapRoute(
null,
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
);
When I call this method:
Url.Action("Details", "Boo", new { id = article.Id, title = article.Title })
I get the following URL:
http://localhost:57553/foo/1/Some%20text%20Š
I would like to create a new route that will lowercase all characters and replace some of them.
e.g.
http://localhost:57553/foo/1/some-text-s
Rules:
Uppercase -> lowercase
' ' -> '-'
'Š' -> 's'
etc.
Any help would be greatly appreciated!
Seems like a perfect candidate for a custom route:
public class MyRoute : Route
{
public MyRoute(string url, object defaultValues)
: base(url, new RouteValueDictionary(defaultValues), new MvcRouteHandler())
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values);
var title = values["title"] as string;
if (!string.IsNullOrEmpty(title))
{
values["title"] = SEOify(title);
}
return base.GetVirtualPath(requestContext, values);
}
private string SEOify(string title)
{
throw new NotImplementedException();
}
}
which will be registered like this:
routes.Add(
"myRoute",
new MyRoute(
"foo/{id}/{title}",
new { controller = "Boo", action = "Details" }
)
);
Now all you have to do is to implement your SEO requirements in the SEOify function that I left. By the way you could get some inspiration from the way StackOverflow does it for the question titles.