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

Added service that use Razor. Added the email templates path structure #624

Merged
merged 6 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -10,6 +10,8 @@
using OutOfSchool.Common.Extensions;
using OutOfSchool.EmailSender;
using OutOfSchool.IdentityServer.ViewModels;
using OutOfSchool.RazorTemplatesData.Models.Emails;
using OutOfSchool.RazorTemplatesData.Services;
using OutOfSchool.Services.Models;

namespace OutOfSchool.IdentityServer.Controllers
Expand All @@ -20,19 +22,23 @@ public class AccountController : Controller
private readonly UserManager<User> userManager;
private readonly IEmailSender emailSender;
private readonly ILogger<AccountController> logger;
private readonly IRazorViewToStringRenderer renderer;

private string userId;
private string path;

public AccountController(
SignInManager<User> signInManager,
UserManager<User> userManager,
IEmailSender emailSender,
ILogger<AccountController> logger)
ILogger<AccountController> logger,
IRazorViewToStringRenderer renderer)
{
this.signInManager = signInManager;
this.userManager = userManager;
this.emailSender = emailSender;
this.logger = logger;
this.renderer = renderer;
}

public override void OnActionExecuting(ActionExecutingContext context)
Expand Down Expand Up @@ -68,7 +74,15 @@ public async Task<IActionResult> ChangeEmail(ChangeEmailViewModel model)

var email = model.Email;
var subject = "Confirm email.";
var htmlMessage = $"Please confirm your email by <a href='{HtmlEncoder.Default.Encode(callBackUrl)}'>clicking here</a>.";

var userActionViewModel = new UserActionViewModel
{
FirstName = user.FirstName,
LastName = user.LastName,
ActionUrl = HtmlEncoder.Default.Encode(callBackUrl),
};
var htmlMessage = await renderer.GetHtmlStringAsync(RazorTemplates.ChangeEmail, userActionViewModel);

await emailSender.SendAsync(email, subject, htmlMessage);

logger.LogInformation($"{path} Confirmation message was sent for User(id) + {userId}.");
Expand Down Expand Up @@ -132,7 +146,15 @@ public async Task<IActionResult> ConfirmEmail()

var email = user.Email;
var subject = "Confirm email.";
var htmlMessage = $"Please confirm your email by <a href='{HtmlEncoder.Default.Encode(callBackUrl)}'>clicking here</a>.";

var userActionViewModel = new UserActionViewModel
{
FirstName = user.FirstName,
LastName = user.LastName,
ActionUrl = HtmlEncoder.Default.Encode(callBackUrl),
};
var htmlMessage = await renderer.GetHtmlStringAsync(RazorTemplates.ConfirmEmail, userActionViewModel);

await emailSender.SendAsync(email, subject, htmlMessage);

logger.LogInformation($"Confirmation message was sent. User(id): {userId}.");
Expand Down Expand Up @@ -205,7 +227,15 @@ public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)

var email = model.Email;
var subject = "Reset Password";
var htmlMessage = $"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callBackUrl)}'>clicking here</a>.";
var userActionViewModel = new UserActionViewModel
{
FirstName = user.FirstName,
LastName = user.LastName,
ActionUrl = HtmlEncoder.Default.Encode(callBackUrl),
};

var htmlMessage = await renderer.GetHtmlStringAsync(RazorTemplates.ResetPassword, userActionViewModel);

await emailSender.SendAsync(email, subject, htmlMessage);

logger.LogInformation($"{path} Message to change password was sent. User(id): {user.Id}.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<ProjectReference Include="..\OutOfSchool.DataAccess\OutOfSchool.DataAccess.csproj" />
<ProjectReference Include="..\OutOfSchool.EmailSender\OutOfSchool.EmailSender.csproj" />
<ProjectReference Include="..\OutOfSchool.Common\OutOfSchool.Common.csproj" />
<ProjectReference Include="..\OutOfSchool.RazorTemplatesData\OutOfSchool.RazorTemplatesData.csproj" />
</ItemGroup>
<ItemGroup>
<Content Remove="stylecop.json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using OutOfSchool.EmailSender;
using OutOfSchool.IdentityServer.Services.Intefaces;
using OutOfSchool.IdentityServer.Services.Password;
using OutOfSchool.RazorTemplatesData.Models.Emails;
using OutOfSchool.RazorTemplatesData.Services;
using OutOfSchool.Services;
using OutOfSchool.Services.Enums;
using OutOfSchool.Services.Models;
Expand All @@ -29,21 +31,24 @@ public class ProviderAdminService : IProviderAdminService

private readonly UserManager<User> userManager;
private readonly OutOfSchoolDbContext context;
private readonly IRazorViewToStringRenderer renderer;

public ProviderAdminService(
IMapper mapper,
IProviderAdminRepository providerAdminRepository,
ILogger<ProviderAdminService> logger,
IEmailSender emailSender,
UserManager<User> userManager,
OutOfSchoolDbContext context)
OutOfSchoolDbContext context,
IRazorViewToStringRenderer renderer)
{
this.mapper = mapper;
this.userManager = userManager;
this.context = context;
this.providerAdminRepository = providerAdminRepository;
this.logger = logger;
this.emailSender = emailSender;
this.renderer = renderer;
}

public async Task<ResponseDto> CreateProviderAdminAsync(
Expand Down Expand Up @@ -148,11 +153,13 @@ await providerAdminRepository.Create(providerAdmin)
new {userId = user.Id, token},
"https");
var subject = "Запрошення!";
var htmlMessage = $"Для реєстрації на платформі перейдіть " +
$"за посиланням та заповність ваші данні в особистому кабінеті.<br>" +
$"{confirmationLink}<br><br>" +
$"Логін: {user.Email}<br>" +
$"Пароль: {password}";
var adminInvitationViewModel = new AdminInvitationViewModel
{
ConfirmationUrl = confirmationLink,
Email = user.Email,
Password = password,
};
var htmlMessage = await renderer.GetHtmlStringAsync(RazorTemplates.NewAdminInvitation, adminInvitationViewModel);

await emailSender.SendAsync(user.Email, subject, htmlMessage);

Expand Down
3 changes: 3 additions & 0 deletions OutOfSchool/OutOfSchool.IdentityServer/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using OutOfSchool.IdentityServer.Services;
using OutOfSchool.IdentityServer.Services.Intefaces;
using OutOfSchool.IdentityServer.Util;
using OutOfSchool.RazorTemplatesData.Services;
using OutOfSchool.Services;
using OutOfSchool.Services.Extensions;
using OutOfSchool.Services.Models;
Expand Down Expand Up @@ -161,6 +162,8 @@ public void ConfigureServices(IServiceCollection services)
// Register the Permission policy handlers
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

services.AddScoped<IRazorViewToStringRenderer, RazorViewToStringRenderer>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace OutOfSchool.RazorTemplatesData.Models.Emails
{
public class AdminInvitationViewModel
{
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmationUrl { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace OutOfSchool.RazorTemplatesData.Models.Emails
{
public class UserActionViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string ActionUrl { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace OutOfSchool.RazorTemplatesData.Models.Shared
{
public class EmailButtonViewModel
{
public EmailButtonViewModel(string Text, string Url)
{
this.Text = Text;
this.Url = Url;
}

public string Text { get; set; }
public string Url { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>


</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;

namespace OutOfSchool.RazorTemplatesData.Services
{
/// <summary>
/// Defines interface for using Razor engine
/// </summary>
public interface IRazorViewToStringRenderer
{
/// <summary>
/// Get rendered string from an html template.
/// </summary>
/// <param name="emailName">Email template name.</param>
/// <param name="model">Data model.</param>
/// <returns>A <see cref="Task{TResult}"/> Rendered an html string.
Task<string> GetHtmlStringAsync<TModel>(string emailName, TModel model);

/// <summary>
/// Get rendered string from an plain text template.
/// </summary>
/// <param name="emailName">Email template name.</param>
/// <param name="model">Data model.</param>
/// <returns>A <see cref="Task{TResult}"/> Rendered an plain text string.

Task<string> GetPlainTextStringAsync<TModel>(string emailName, TModel model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace OutOfSchool.RazorTemplatesData.Services
{
public static class RazorTemplates
{
/// <summary>
/// Base path to the html email templates
/// </summary>
private static string MainPath => "/Views/Emails/";

/// <summary>
/// Base path to the plain text email templates
/// </summary>
private static string MainPathPlainText => "/ViewsPlainText/Emails/";

// Supported email templates
public static string ConfirmEmail => "ConfirmEmail";
public static string ChangeEmail => "ChangeEmail";
public static string ResetPassword => "ResetPassword";
public static string NewAdminInvitation => "NewAdminInvitation";

/// <summary>
/// Get view name
/// </summary>
/// <param name="emailName">Email template name</param>
/// <param name="isHtml">Indicates which template will be used: true = html, false - plain text.</param>
/// <returns>View name</returns>
internal static string GetViewName(string emailName, bool isHtml = true)
=> $"{(isHtml ? MainPath : MainPathPlainText)}{emailName}/{emailName}.cshtml";

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace OutOfSchool.RazorTemplatesData.Services
{
public class RazorViewToStringRenderer : IRazorViewToStringRenderer
{
private readonly IRazorViewEngine viewEngine;
private readonly ITempDataProvider tempDataProvider;
private readonly IServiceProvider serviceProvider;

public RazorViewToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
this.viewEngine = viewEngine;
this.tempDataProvider = tempDataProvider;
this.serviceProvider = serviceProvider;
}

/// <inheritdoc/>
public async Task<string> GetHtmlStringAsync<TModel>(string emailName, TModel model)
{
var viewName = RazorTemplates.GetViewName(emailName);

return await RenderViewToStringAsync(viewName, model);
}

/// <inheritdoc/>
public async Task<string> GetPlainTextStringAsync<TModel>(string emailName, TModel model)
{
var viewName = RazorTemplates.GetViewName(emailName, false);

return await RenderViewToStringAsync(viewName, model);
}

private async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var view = FindView(actionContext, viewName);

await using var output = new StringWriter();
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
tempDataProvider),
output,
new HtmlHelperOptions());

await view.RenderAsync(viewContext);

return output.ToString();
}

private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
{
return getViewResult.View;
}

var findViewResult = viewEngine.FindView(actionContext, viewName, isMainPage: true);
if (findViewResult.Success)
{
return findViewResult.View;
}

var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));

throw new InvalidOperationException(errorMessage);
}

private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
}
Loading