(feature): custom json model binder

This commit is contained in:
Maksym Sadovnychyy 2023-02-06 22:02:49 +01:00
parent cac3d2502e
commit 023d3b065c
32 changed files with 180 additions and 25 deletions

View File

@ -10,7 +10,7 @@
"address": {
"street": "123 456th St",
"city": "New York",
"region": "NY"
"region": "NY",
"postCode": "10001",
"country": "US"
},
@ -21,7 +21,7 @@
"passwordRecoverySettings": {
"smtpSettings": {
"server": "smtp.ionos.it",
"port": 587
"port": 587,
"useSsl": true,
"userName": "commercial@maks-it.com",
"password": "nECbzrWqwzM5Lv4zCxV91g=="
@ -41,7 +41,6 @@
"paragraphs": [
"Your recovery link: {{recoveryLink}}"
]
}
}
]

View File

@ -114,7 +114,10 @@
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top">
{{paragraphs}}
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Hello, {{userName}}</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">We have recieved your account password recovery request.<br />
In case if it wasn't you, please ignore this email.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">Your password recovery token: {{passwordRecoveryToken}}</p>
</td>
</tr>
</table>

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Binders {
using System;
using System.Threading.Tasks;
using Extensions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class JsonModelBinder<T> : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException(nameof(bindingContext));
}
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None) {
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = valueAsString.ToObject<T>();
if (result != null) {
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Core.Abstractions;
namespace Core.Converters;
public class EnumerationDisplayNameConverter<T> : JsonConverter<T> where T : Enumeration {
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
Enumeration.FromDisplayName<T>(reader.GetString() ?? "");
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.Name);
}

View File

@ -0,0 +1,13 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Core.Abstractions;
namespace Core.Converters;
public class EnumerationIdConverter<T> : JsonConverter<T> where T : Enumeration {
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
Enumeration.FromValue<T>(reader.GetInt32());
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
writer.WriteNumberValue(value.Id);
}

View File

@ -13,7 +13,7 @@ namespace Core.Enumerations {
public static readonly Errors WrongOrNotManaged = new(2, "is wrong or not managed");
public static readonly Errors NullOrEmpty = new(3, "is null or empty");
public static readonly Errors NotMatched = new(4, "not matched");
public static readonly Errors UnableToParse = new(5, "unable to parse");
private Errors(int id, string displayName) : base(id, displayName) { }
}

View File

@ -0,0 +1,20 @@
using Core.Abstractions;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Enumerations;
public class TemplateTypes : Enumeration {
public static readonly TemplateTypes Unknown = new(-1, "Unknown");
public static readonly TemplateTypes PasswordRecovery = new(0, "PasswordRecovery");
public static readonly TemplateTypes EmailConfirmation = new(1, "EmailConfirmation");
private TemplateTypes(int id, string displayName) : base(id, displayName) { }
}

View File

@ -52,6 +52,8 @@ public class Password : DomainObjectBase<Password> {
/// <returns></returns>
public string Recovery() {
var recoveryToken = new PasswordRecoveryToken();
RecoveryTokens ??= new List<PasswordRecoveryToken>();
RecoveryTokens.Add(recoveryToken);
return recoveryToken.Value;
@ -64,7 +66,7 @@ public class Password : DomainObjectBase<Password> {
/// <param name="password"></param>
/// <returns></returns>
public bool Reset(string token, string password) {
if (RecoveryTokens.Any(x => x.Value == token && !(DateTime.UtcNow > x.Expires))) {
if (RecoveryTokens != null && RecoveryTokens.Any(x => x.Value == token && !(DateTime.UtcNow > x.Expires))) {
Set(password);
RecoveryTokens.Clear();

View File

@ -7,8 +7,6 @@ using WeatherForecast.Models.Account.Requests;
using WeatherForecast.Policies;
using WeatherForecast.Services;
using DomainObjects.Documents.Users;
using Microsoft.AspNetCore.Identity;
using Org.BouncyCastle.Asn1.Ocsp;
using DomainResults.Common;
namespace WeatherForecast.Controllers;
@ -107,7 +105,7 @@ public class AccountController : ControllerBase {
if ((await _authorizationService.AuthorizeAsync(User, new List<User> { user }, new PasswordChangeRequirement {
OldPassword = requestData.OldPassword
})).Succeeded) {
var result = _accountService.PasswordChange(user, requestData.OldPassword, requestData.NewPassword);
var result = _accountService.PasswordChange(user, requestData);
return result.ToActionResult();
}

View File

@ -7,6 +7,8 @@ using DomainResults.Mvc;
using DataProviders.Buckets;
using WeatherForecast.Services;
using WeatherForecast.Models.Template.Requests;
using Core.Binders;
namespace WeatherForecast.Controllers;
@ -38,10 +40,11 @@ public class TemplateController : ControllerBase {
/// Allows to upload private dkim certificate
/// </summary>
/// <param name="siteId"></param>
/// <param name="requestData"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpPost("{siteId}")]
public IActionResult Post([FromRoute] Guid siteId, IFormFile file) {
public IActionResult Post([FromRoute] Guid siteId, [ModelBinder(typeof(JsonModelBinder<PostTemplateRequestModel>))] PostTemplateRequestModel requestData, IFormFile file) {
if (!(file.Length > 0))
return IDomainResult.Failed().ToActionResult();
@ -49,11 +52,35 @@ public class TemplateController : ControllerBase {
using var ms = new MemoryStream();
file.CopyTo(ms);
var result = _templateService.Post(new BucketFile(siteId, file.FileName, ms.ToArray(), file.ContentType));
var result = _templateService.Post(requestData, new BucketFile(siteId, file.FileName, ms.ToArray(), file.ContentType));
return result.ToActionResult();
}
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="fileId"></param>
/// <returns></returns>
[HttpGet("{siteId}/{fileId}")]
public IActionResult Get([FromRoute] Guid siteId, [FromRoute] Guid fileId) {
return BadRequest();
}
/// <summary>
///
/// </summary>
/// <param name="siteId"></param>
/// <param name="fileId"></param>
/// <param name="requestData"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpGet("{siteId}/{fileId}")]
public IActionResult Put([FromRoute] Guid siteId, [FromRoute] Guid fileId, [ModelBinder(typeof(JsonModelBinder<PostTemplateRequestModel>))] PostTemplateRequestModel requestData, IFormFile file) {
return BadRequest();
}
/// <summary>
/// Delete template

View File

@ -12,12 +12,12 @@ namespace WeatherForecast.Models.Account.Requests {
/// <summary>
///
/// </summary>
public string? OldPassword { get; set; }
public string OldPassword { get; set; } = string.Empty;
/// <summary>
///
/// </summary>
public string? NewPassword { get; set; }
public string NewPassword { get; set; } = string.Empty;
/// <summary>
///

View File

@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using System.ComponentModel.DataAnnotations;
using Core.Abstractions.Models;
using Core.Converters;
using Core.Enumerations;
namespace WeatherForecast.Models.Template.Requests {
/// <summary>
///
/// </summary>
public class PostTemplateRequestModel : RequestModelBase {
/// <summary>
///
/// </summary>
[JsonConverter(typeof(EnumerationDisplayNameConverter<TemplateTypes>))]
public TemplateTypes TemplateType { get; set; } = TemplateTypes.Unknown;
/// <summary>
///
/// </summary>
/// <param name="validationContext"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (TemplateType == TemplateTypes.Unknown)
yield return new ValidationResult($"{Errors.UnableToParse.Name} {nameof(TemplateType)}");
}
}
}

View File

@ -44,10 +44,9 @@ namespace WeatherForecast.Services {
///
/// </summary>
/// <param name="user"></param>
/// <param name="oldPassword"></param>
/// <param name="newPassword"></param>
/// <param name="requestData"></param>
/// <returns></returns>
(Guid?, IDomainResult) PasswordChange(User user, string oldPassword, string newPassword);
(Guid?, IDomainResult) PasswordChange(User user, PasswordChangeRequestModel requestData);
}
/// <summary>
@ -164,7 +163,12 @@ namespace WeatherForecast.Services {
var htmlBody = Encoding.UTF8.GetString(template.Bytes);
htmlBody = htmlBody.Replace("{{token}}", user.Authentication.PasswordRecovery());
htmlBody = htmlBody
.Replace("{{subject}}", site.PassworRecoverySettings.Subject)
.Replace("{{userName}}", user.Username)
.Replace("{{passwordRecoveryToken}}", user.Authentication.PasswordRecovery())
.Replace("{{poweredBy.target}}", site.PoweredBy.Target)
.Replace("{{poweredBy.anchorText}}", site.PoweredBy.AnchorText);
_userDataProvider.Update(user);
@ -210,12 +214,11 @@ namespace WeatherForecast.Services {
///
/// </summary>
/// <param name="user"></param>
/// <param name="oldPassword"></param>
/// <param name="newPassword"></param>
/// <param name="requestData"></param>
/// <returns></returns>
public (Guid?, IDomainResult) PasswordChange(User user, string oldPassword, string newPassword) {
public (Guid?, IDomainResult) PasswordChange(User user, PasswordChangeRequestModel requestData) {
user.Authentication.PasswordChange(oldPassword, newPassword);
user.Authentication.PasswordChange(requestData.OldPassword, requestData.NewPassword);
var (userId, userUpdateResult) = _userDataProvider.Update(user);
if (!userUpdateResult.IsSuccess || userId == null)

View File

@ -1,7 +1,11 @@
using Core.Abstractions;
using DataProviders.Buckets;
using DomainResults.Common;
using DataProviders.Buckets;
using WeatherForecast.Models.Template.Requests;
namespace WeatherForecast.Services;
/// <summary>
@ -12,9 +16,10 @@ public interface ITemplateService {
/// <summary>
///
/// </summary>
/// <param name="requestData"></param>
/// <param name="file"></param>
/// <returns></returns>
(Guid?, IDomainResult) Post(BucketFile file);
(Guid?, IDomainResult) Post(PostTemplateRequestModel requestData, BucketFile file);
/// <summary>
///
@ -48,9 +53,10 @@ public class TemplateService : ServiceBase<TemplateService>, ITemplateService {
/// <summary>
///
/// </summary>
/// <param name="requestData"></param>
/// <param name="file"></param>
/// <returns></returns>
public (Guid?, IDomainResult) Post(BucketFile file) {
public (Guid?, IDomainResult) Post(PostTemplateRequestModel requestData, BucketFile file) {
var (fileId, uploadFileResult) = _templateBucketDataProvider.Upload(file);
if (!uploadFileResult.IsSuccess || fileId == null)

View File

@ -3,4 +3,4 @@ WiredTiger 10.0.2: (December 21, 2021)
WiredTiger version
major=10,minor=0,patch=2
file:WiredTiger.wt
access_pattern_hint=none,allocation_size=4KB,app_metadata=,assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,collator=,columns=,dictionary=0,encryption=(keyid=,name=),format=btree,huffman_key=,huffman_value=,id=0,ignore_in_memory_cache_size=false,internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=4KB,key_format=S,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=32KB,leaf_value_max=0,log=(enabled=true),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=false,prefix_compression_min=4,readonly=false,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,cache_directory=,local_retention=300,name=,object_target_size=0),value_format=S,verbose=[],version=(major=1,minor=1),write_timestamp_usage=none,checkpoint=(WiredTigerCheckpoint.99539=(addr="018081e4760268df8181e4e314ef2b8281e4f0cb9f65808080e301ffc0e3010fc0",order=99539,time=1674424019,size=81920,newest_start_durable_ts=0,oldest_start_ts=0,newest_txn=3110,newest_stop_durable_ts=0,newest_stop_ts=-1,newest_stop_txn=-11,prepare=0,write_gen=299042,run_write_gen=294763)),checkpoint_backup_info=,checkpoint_lsn=(38,1188480)
access_pattern_hint=none,allocation_size=4KB,app_metadata=,assert=(commit_timestamp=none,durable_timestamp=none,read_timestamp=none,write_timestamp=off),block_allocation=best,block_compressor=,cache_resident=false,checksum=on,collator=,columns=,dictionary=0,encryption=(keyid=,name=),format=btree,huffman_key=,huffman_value=,id=0,ignore_in_memory_cache_size=false,internal_item_max=0,internal_key_max=0,internal_key_truncate=true,internal_page_max=4KB,key_format=S,key_gap=10,leaf_item_max=0,leaf_key_max=0,leaf_page_max=32KB,leaf_value_max=0,log=(enabled=true),memory_page_image_max=0,memory_page_max=5MB,os_cache_dirty_max=0,os_cache_max=0,prefix_compression=false,prefix_compression_min=4,readonly=false,split_deepen_min_child=0,split_deepen_per_child=0,split_pct=90,tiered_object=false,tiered_storage=(auth_token=,bucket=,bucket_prefix=,cache_directory=,local_retention=300,name=,object_target_size=0),value_format=S,verbose=[],version=(major=1,minor=1),write_timestamp_usage=none,checkpoint=(WiredTigerCheckpoint.101439=(addr="019181e43493ac119281e4c3feb5439381e4de446160808080e301ffc0e3010fc0",order=101439,time=1675717350,size=81920,newest_start_durable_ts=0,oldest_start_ts=0,newest_txn=462,newest_stop_durable_ts=0,newest_stop_ts=-1,newest_stop_txn=-11,prepare=0,write_gen=304767,run_write_gen=304099)),checkpoint_backup_info=,checkpoint_lsn=(40,185856)