Refactored Communications and Evidence endpoints
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Events.Communications;
|
||||
|
||||
public class SyncUserEvent : EventBase, IEvent
|
||||
{
|
||||
public string Name { get; set; } = nameof(SyncUserEvent);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using PostFundManagement.Domain.Configuration.Email;
|
||||
using PostFundManagement.Domain.Services;
|
||||
using PostFundManagement.Domain.Services.Shared;
|
||||
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
|
||||
public static class General
|
||||
{
|
||||
public static ProblemDetails ToProblems(this IReadOnlyList<IError> errors, string title) => new()
|
||||
{
|
||||
Errors = errors.Select(e => new KeyValuePair<string, string[]>(e.Message,
|
||||
[.. e.Reasons.Select(r => r.Message)])).ToDictionary(),
|
||||
Detail = errors.FirstOrDefault()?.Message,
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Title = title,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Services;
|
||||
using PostFundManagement.Domain.Services.Shared;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace PostFundManagement.Domain;
|
||||
|
||||
public sealed class Pagination
|
||||
{
|
||||
const int defaultPage = 1;
|
||||
const int defaultPageSize = 100;
|
||||
const long defaultIndex = 0;
|
||||
|
||||
public long Index { get; set; }
|
||||
|
||||
public int Page { get; set; }
|
||||
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public Pagination()
|
||||
{
|
||||
Page = defaultPage;
|
||||
PageSize = defaultPageSize;
|
||||
Index = defaultIndex;
|
||||
}
|
||||
|
||||
public Pagination(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex)
|
||||
{
|
||||
if (page < 1) page = defaultPage;
|
||||
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize;
|
||||
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
public static Pagination Create(int page = defaultPage, int pageSize = defaultPageSize, long index = defaultIndex)
|
||||
{
|
||||
if (page < 1) page = defaultPage;
|
||||
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = defaultPageSize;
|
||||
|
||||
return new(page, pageSize, index);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class BrowserLocalStorageService(ProtectedLocalStorage storage)
|
||||
{
|
||||
public async ValueTask<Result> DeleteAsync(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
await storage.DeleteAsync(key);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAsync(string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await storage.SetAsync(key, value);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> SaveAsync<TValue>(string key, TValue value) where TValue : class
|
||||
{
|
||||
try
|
||||
{
|
||||
await storage.SetAsync(key, value);
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<string>> GetAsync(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var retrieval = await storage.GetAsync<string>(key);
|
||||
|
||||
return retrieval.Success && !string.IsNullOrWhiteSpace(retrieval.Value)
|
||||
? Result.Ok(retrieval.Value)
|
||||
: Result.Fail($"Could not find object by key {key}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<TValue>> GetAsync<TValue>(string key) where TValue : class
|
||||
{
|
||||
try
|
||||
{
|
||||
var retrieval = await storage.GetAsync<TValue>(key);
|
||||
|
||||
return retrieval.Success && retrieval.Value is not null
|
||||
? Result.Ok(retrieval.Value)
|
||||
: Result.Fail($"Could not find object by key {key}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class ContractS3Service(IConfiguration configuration, [FromKeyedServices(ContractBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{ContractS3SettingsSection}:BucketName").Value ?? "";
|
||||
protected override string CdnBaseUrl => configuration.GetSection($"{ContractS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using PostFundManagement.Domain.Configuration.Email;
|
||||
using PostFundManagement.Domain.Extensions;
|
||||
using PostFundManagement.Domain.Models.Email;
|
||||
using static PostFundManagement.Domain.Extensions.EmailTelemetry;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class EmailService(IOptions<SmtpSettings> options) : IDisposable
|
||||
{
|
||||
private readonly SmtpSettings settings = options.Value;
|
||||
|
||||
private readonly SmtpClient client = new();
|
||||
|
||||
private readonly int sendMaxCount = 10;
|
||||
|
||||
private int sendCount = 0;
|
||||
|
||||
public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected;
|
||||
|
||||
public async ValueTask<Result<Response>> SendEmailAsync(Message message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = EmailTelemetry.Source.StartActivity("Email Send");
|
||||
|
||||
activity?.SetTag("email.recipient", message.Recipient?.Address);
|
||||
|
||||
try
|
||||
{
|
||||
if (Status != EmailStatuses.Connected)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "Disconnected");
|
||||
|
||||
return Result.Fail<Response>("Smtp service is disconnected.");
|
||||
}
|
||||
|
||||
var email = ConstructEmail(message, cancellationToken);
|
||||
|
||||
var response = await client.SendAsync(email, cancellationToken);
|
||||
|
||||
bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
message.Dispose();
|
||||
|
||||
Interlocked.Increment(ref sendCount);
|
||||
|
||||
if (sendCount % sendMaxCount == 0)
|
||||
{
|
||||
using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause");
|
||||
|
||||
sendCount = 0;
|
||||
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
|
||||
if (emailSent)
|
||||
{
|
||||
EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } });
|
||||
|
||||
return Result.Ok(Response.Create(EmailStatuses.Success));
|
||||
}
|
||||
|
||||
await DisconnectAsync(cancellationToken);
|
||||
|
||||
var failCheckResult = HandleNegativeResponse(response);
|
||||
|
||||
if (failCheckResult.IsFailed) return failCheckResult;
|
||||
|
||||
Status = EmailStatuses.Disconnected;
|
||||
|
||||
return Result.Fail<Response>("General error, disconnected");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
activity?.AddException(ex);
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
|
||||
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static MimeMessage ConstructEmail(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
var email = new MimeMessage();
|
||||
email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!));
|
||||
email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!));
|
||||
email.Subject = message.Subject!;
|
||||
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
|
||||
if (message.Body!.Properties.HasAttachments)
|
||||
foreach (var attachment in message.Body?.Attachments!)
|
||||
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
|
||||
|
||||
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
|
||||
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
|
||||
|
||||
email.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
private Result<Response> HandleNegativeResponse(string response)
|
||||
{
|
||||
if (response.Contains("421", StringComparison.Ordinal))
|
||||
{
|
||||
Status = EmailStatuses.TooManyConnections;
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
if (response.Contains("451", StringComparison.Ordinal))
|
||||
{
|
||||
Status = EmailStatuses.ConnectionAborted;
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
|
||||
|
||||
return Result.Fail<Response>(response);
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Response>> ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = EmailTelemetry.Source.StartActivity("Email Connect");
|
||||
activity?.SetTag("email.smtp.connect", settings.Host);
|
||||
|
||||
try
|
||||
{
|
||||
if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status));
|
||||
|
||||
await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken);
|
||||
await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken);
|
||||
|
||||
Status = EmailStatuses.Connected;
|
||||
|
||||
activity?.SetStatus(ActivityStatusCode.Ok, "Connected");
|
||||
|
||||
return Result.Ok(Response.Create(Status));
|
||||
}
|
||||
catch (MailKit.ProtocolException ex)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
activity?.AddException(ex);
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
|
||||
|
||||
Status = EmailStatuses.ProtocolError;
|
||||
|
||||
return Result.Fail<Response>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
activity?.AddException(ex);
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
|
||||
|
||||
Status = EmailStatuses.AuthenticationError;
|
||||
|
||||
return Result.Fail<Response>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
activity?.AddException(ex);
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
|
||||
|
||||
Status = EmailStatuses.GeneralError;
|
||||
|
||||
return Result.Fail<Response>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect");
|
||||
activity?.SetTag("email.smtp.disconnect", settings.Host);
|
||||
|
||||
try
|
||||
{
|
||||
if (Status is EmailStatuses.Disconnected) return Result.Ok();
|
||||
|
||||
await client.DisconnectAsync(true, cancellationToken);
|
||||
|
||||
activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected");
|
||||
|
||||
Status = EmailStatuses.Disconnected;
|
||||
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
activity?.AddException(ex);
|
||||
|
||||
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } });
|
||||
|
||||
Status = EmailStatuses.GeneralError;
|
||||
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
client.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class EvidenceS3Service(IConfiguration configuration, [FromKeyedServices(EvidenceBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{EvidenceS3SettingsSection}:BucketName").Value ?? "";
|
||||
protected override string CdnBaseUrl => configuration.GetSection($"{EvidenceS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class GeneralS3Service(IConfiguration configuration, [FromKeyedServices(GeneralBucketName)] IAmazonS3 amazonS3) :
|
||||
S3ServiceBase(amazonS3), IS3Service
|
||||
{
|
||||
protected override string BucketName => configuration.GetSection($"{GeneralS3SettingsSection}:BucketName").Value ?? "";
|
||||
protected override string CdnBaseUrl => configuration.GetSection($"{GeneralS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed partial class HashService(IHashids hasher) : IService
|
||||
{
|
||||
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
private static partial Regex HexHashRegex { get; }
|
||||
|
||||
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
private static partial Regex Md5Regex { get; }
|
||||
|
||||
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||
private static partial Regex Sha256Regex { get; }
|
||||
|
||||
public static bool IsMd5Hash(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
|
||||
|
||||
public static bool IsSha256Hash(string? value) =>
|
||||
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
|
||||
|
||||
public static string? StringToSha256Hash(string? input) =>
|
||||
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||
|
||||
public static string? StreamToSha256Hash(Stream stream) =>
|
||||
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
||||
|
||||
public static string? BytesToSha256Hash(byte[] bytes) =>
|
||||
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
||||
|
||||
public static Result<string> ToMd5Hash(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||
|
||||
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||
}
|
||||
|
||||
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||
: Result.Ok(hasher.EncodeHex(input));
|
||||
|
||||
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||
? Result.Fail<string>("Id cannot be negative.")
|
||||
: Result.Ok(hasher.Encode(id));
|
||||
|
||||
public Result<string> HashEncodeLongId(long id) => id < 0
|
||||
? Result.Fail<string>("Id cannot be negative.")
|
||||
: Result.Ok(hasher.EncodeLong(id));
|
||||
|
||||
public Result<int> DecodeIntIdHash(string hash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<int>("Invalid token layout.");
|
||||
|
||||
int[] decoded = hasher.Decode(hash);
|
||||
|
||||
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<int>("Invalid or modified Int hash token.");
|
||||
}
|
||||
|
||||
public Result<long> DecodeLongIdHash(string hash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<long>("Invalid token layout.");
|
||||
|
||||
long[] decoded = hasher.DecodeLong(hash);
|
||||
|
||||
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<long>("Invalid or modified Long hash token.");
|
||||
}
|
||||
|
||||
public Result<string> DecodeHexHash(string hex)
|
||||
{
|
||||
try
|
||||
{
|
||||
string decoded = hasher.DecodeHex(hex);
|
||||
|
||||
return string.IsNullOrEmpty(decoded)
|
||||
? Result.Fail<string>("Invalid or corrupted hex hash.")
|
||||
: Result.Ok(decoded);
|
||||
}
|
||||
catch (FormatException fex)
|
||||
{
|
||||
return Result.Fail<string>(new Error("Invalid hash structure.").CausedBy(fex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<string>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using PostFundManagement.Domain.Api.Configuration;
|
||||
using PostFundManagement.Domain.Api.Models;
|
||||
using PostFundManagement.Domain.Sdk;
|
||||
|
||||
namespace PostFundManagement.Domain.Services;
|
||||
|
||||
public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<SecurityClientSettings> clientOptions)
|
||||
{
|
||||
private readonly SecurityClientSettings clientSettings = clientOptions.Value;
|
||||
|
||||
public async Task<Result<Api.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new Api.Models.TokenRequest
|
||||
{
|
||||
ClientId = clientSettings.ClientId,
|
||||
ClientSecret = clientSettings.ClientSecret,
|
||||
GrantType = clientSettings.GrantType,
|
||||
Scope = clientSettings.Scope,
|
||||
};
|
||||
|
||||
using var response = await connectApi.GetToken(request, cancellationToken);
|
||||
|
||||
var contentRaw = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(contentRaw))
|
||||
return Result.Fail(new Error($"The authentication endpoint returned an empty payload. Status code: {response.StatusCode}"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var tokenResponse = JsonSerializer.Deserialize<Api.Models.TokenResponse>(contentRaw);
|
||||
|
||||
return !string.IsNullOrWhiteSpace(tokenResponse?.AccessToken)
|
||||
? Result.Ok(tokenResponse)
|
||||
: Result.Fail<Api.Models.TokenResponse>(new Error("Authentication succeeded, but no access token was found in the response payload."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
|
||||
|
||||
if (errorResult != null)
|
||||
{
|
||||
string summary = $"{errorResult.Error}: {errorResult.ErrorDescription}";
|
||||
|
||||
return Result.Fail(new Error(summary));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Result.Fail(new Error($"Authentication failed: {contentRaw}"));
|
||||
}
|
||||
|
||||
return Result.Fail(new Error($"Authentication failed with status code: {response.StatusCode}"));
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
return Result.Fail(new Error("The token generation request was canceled.").CausedBy(ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user