mirror of https://github.com/abpframework/abp.git
143 changed files with 3645 additions and 2486 deletions
@ -0,0 +1,53 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Confluent.Kafka; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Kafka |
||||
|
{ |
||||
|
public class KafkaEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
protected ILogger<KafkaEventErrorHandler> Logger { get; set; } |
||||
|
|
||||
|
public KafkaEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) : base(options) |
||||
|
{ |
||||
|
Logger = NullLogger<KafkaEventErrorHandler>.Instance; |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
|
||||
|
await context.EventBus.As<KafkaDistributedEventBus>().PublishAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<Headers>(), |
||||
|
new Dictionary<string, object> {{RetryAttemptKey, ++retryAttempt}}); |
||||
|
} |
||||
|
|
||||
|
protected override async Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
Logger.LogException( |
||||
|
context.Exceptions.Count == 1 ? context.Exceptions.First() : new AggregateException(context.Exceptions), |
||||
|
LogLevel.Error); |
||||
|
|
||||
|
await context.EventBus.As<KafkaDistributedEventBus>().PublishToDeadLetterAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<Headers>(), |
||||
|
new Dictionary<string, object> {{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()}}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using RabbitMQ.Client; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.RabbitMq |
||||
|
{ |
||||
|
public class RabbitMqEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
public RabbitMqEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
|
||||
|
await context.EventBus.As<RabbitMqDistributedEventBus>().PublishAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<IBasicProperties>(), |
||||
|
new Dictionary<string, object> |
||||
|
{ |
||||
|
{RetryAttemptKey, ++retryAttempt}, |
||||
|
{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Rebus |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Rebus will automatic retries and error handling: https://github.com/rebus-org/Rebus/wiki/Automatic-retries-and-error-handling
|
||||
|
/// </summary>
|
||||
|
public class RebusEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
public RebusEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class AbpEventBusOptions |
||||
|
{ |
||||
|
public bool EnabledErrorHandle { get; set; } |
||||
|
|
||||
|
public Func<Type, bool> ErrorHandleSelector { get; set; } |
||||
|
|
||||
|
public string DeadLetterName { get; set; } |
||||
|
|
||||
|
public AbpEventBusRetryStrategyOptions RetryStrategyOptions { get; set; } |
||||
|
|
||||
|
public void UseRetryStrategy(Action<AbpEventBusRetryStrategyOptions> action = null) |
||||
|
{ |
||||
|
EnabledErrorHandle = true; |
||||
|
RetryStrategyOptions = new AbpEventBusRetryStrategyOptions(); |
||||
|
action?.Invoke(RetryStrategyOptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class AbpEventBusRetryStrategyOptions |
||||
|
{ |
||||
|
public int IntervalMillisecond { get; set; } = 3000; |
||||
|
|
||||
|
public int MaxRetryAttempts { get; set; } = 3; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public abstract class EventErrorHandlerBase : IEventErrorHandler |
||||
|
{ |
||||
|
public const string HeadersKey = "headers"; |
||||
|
public const string RetryAttemptKey = "retryAttempt"; |
||||
|
|
||||
|
protected AbpEventBusOptions Options { get; } |
||||
|
|
||||
|
protected EventErrorHandlerBase(IOptions<AbpEventBusOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task HandleAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (!await ShouldHandleAsync(context)) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
} |
||||
|
|
||||
|
if (await ShouldRetryAsync(context)) |
||||
|
{ |
||||
|
await RetryAsync(context); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await MoveToDeadLetterAsync(context); |
||||
|
} |
||||
|
|
||||
|
protected abstract Task RetryAsync(EventExecutionErrorContext context); |
||||
|
|
||||
|
protected abstract Task MoveToDeadLetterAsync(EventExecutionErrorContext context); |
||||
|
|
||||
|
protected virtual Task<bool> ShouldHandleAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (!Options.EnabledErrorHandle) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(Options.ErrorHandleSelector == null || Options.ErrorHandleSelector.Invoke(context.EventType)); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions == null) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
if (!context.TryGetRetryAttempt(out var retryAttempt)) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(Options.RetryStrategyOptions.MaxRetryAttempts > retryAttempt); |
||||
|
} |
||||
|
|
||||
|
protected virtual void ThrowOriginalExceptions(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (context.Exceptions.Count == 1) |
||||
|
{ |
||||
|
context.Exceptions[0].ReThrow(); |
||||
|
} |
||||
|
|
||||
|
throw new AggregateException( |
||||
|
"More than one error has occurred while triggering the event: " + context.EventType, |
||||
|
context.Exceptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.ObjectExtending; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class EventExecutionErrorContext : ExtensibleObject |
||||
|
{ |
||||
|
public IReadOnlyList<Exception> Exceptions { get; } |
||||
|
|
||||
|
public object EventData { get; set; } |
||||
|
|
||||
|
public Type EventType { get; } |
||||
|
|
||||
|
public IEventBus EventBus { get; } |
||||
|
|
||||
|
public EventExecutionErrorContext(List<Exception> exceptions, Type eventType, IEventBus eventBus) |
||||
|
{ |
||||
|
Exceptions = exceptions; |
||||
|
EventType = eventType; |
||||
|
EventBus = eventBus; |
||||
|
} |
||||
|
|
||||
|
public bool TryGetRetryAttempt(out int retryAttempt) |
||||
|
{ |
||||
|
retryAttempt = 0; |
||||
|
if (!this.HasProperty(EventErrorHandlerBase.RetryAttemptKey)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
retryAttempt = this.GetProperty<int>(EventErrorHandlerBase.RetryAttemptKey); |
||||
|
return true; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public interface IEventErrorHandler |
||||
|
{ |
||||
|
Task HandleAsync(EventExecutionErrorContext context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
[ExposeServices(typeof(LocalEventErrorHandler), typeof(IEventErrorHandler))] |
||||
|
public class LocalEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
protected Dictionary<Guid, int> RetryTracking { get; } |
||||
|
|
||||
|
public LocalEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
RetryTracking = new Dictionary<Guid, int>(); |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
RetryTracking[messageId] = ++retryAttempt; |
||||
|
|
||||
|
await context.EventBus.As<LocalEventBus>().PublishAsync(new LocalEventMessage(messageId, context.EventData, context.EventType)); |
||||
|
|
||||
|
RetryTracking.Remove(messageId); |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected override async Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
||||
|
context.SetProperty(RetryAttemptKey, RetryTracking.GetOrDefault(messageId)); |
||||
|
|
||||
|
if (await base.ShouldRetryAsync(context)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
RetryTracking.Remove(messageId); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
public class LocalEventMessage |
||||
|
{ |
||||
|
public Guid MessageId { get; } |
||||
|
|
||||
|
public object EventData { get; } |
||||
|
|
||||
|
public Type EventType { get; } |
||||
|
|
||||
|
public LocalEventMessage(Guid messageId, object eventData, Type eventType) |
||||
|
{ |
||||
|
MessageId = messageId; |
||||
|
EventData = eventData; |
||||
|
EventType = eventType; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
public class EventBus_Exception_Handler_Tests : EventBusTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Handle_Exception() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MySimpleEventData>(eventData => |
||||
|
{ |
||||
|
retryAttempt++; |
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
}); |
||||
|
|
||||
|
var appException = await Assert.ThrowsAsync<Exception>(async () => |
||||
|
{ |
||||
|
await LocalEventBus.PublishAsync(new MySimpleEventData(1)); |
||||
|
}); |
||||
|
|
||||
|
retryAttempt.ShouldBe(1); |
||||
|
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Handle_Exception() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
||||
|
{ |
||||
|
eventData.Value.ShouldBe(0); |
||||
|
|
||||
|
retryAttempt++; |
||||
|
eventData.Value++; |
||||
|
if (retryAttempt < 2) |
||||
|
{ |
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
||||
|
retryAttempt.ShouldBe(2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Throw_Exception_After_Error_Handle() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
||||
|
{ |
||||
|
eventData.Value.ShouldBe(0); |
||||
|
|
||||
|
retryAttempt++; |
||||
|
eventData.Value++; |
||||
|
|
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
}); |
||||
|
|
||||
|
var appException = await Assert.ThrowsAsync<Exception>(async () => |
||||
|
{ |
||||
|
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
||||
|
}); |
||||
|
|
||||
|
retryAttempt.ShouldBe(4); |
||||
|
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class MyExceptionHandleEventData |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
|
||||
|
public MyExceptionHandleEventData(int value) |
||||
|
{ |
||||
|
Value = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.AspNetCore.ExceptionHandling; |
||||
|
using Volo.Abp.Auditing; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.Http; |
||||
|
using Volo.Abp.Json; |
||||
|
|
||||
|
namespace Volo.Abp.AuditLogging |
||||
|
{ |
||||
|
public class AuditLogInfoToAuditLogConverter : IAuditLogInfoToAuditLogConverter, ITransientDependency |
||||
|
{ |
||||
|
protected IGuidGenerator GuidGenerator { get; } |
||||
|
protected IExceptionToErrorInfoConverter ExceptionToErrorInfoConverter { get; } |
||||
|
protected IJsonSerializer JsonSerializer { get; } |
||||
|
|
||||
|
public AuditLogInfoToAuditLogConverter(IGuidGenerator guidGenerator, IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IJsonSerializer jsonSerializer) |
||||
|
{ |
||||
|
GuidGenerator = guidGenerator; |
||||
|
ExceptionToErrorInfoConverter = exceptionToErrorInfoConverter; |
||||
|
JsonSerializer = jsonSerializer; |
||||
|
} |
||||
|
|
||||
|
public virtual Task<AuditLog> ConvertAsync(AuditLogInfo auditLogInfo) |
||||
|
{ |
||||
|
var auditLogId = GuidGenerator.Create(); |
||||
|
|
||||
|
var extraProperties = new ExtraPropertyDictionary(); |
||||
|
if (auditLogInfo.ExtraProperties != null) |
||||
|
{ |
||||
|
foreach (var pair in auditLogInfo.ExtraProperties) |
||||
|
{ |
||||
|
extraProperties.Add(pair.Key, pair.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var entityChanges = auditLogInfo |
||||
|
.EntityChanges? |
||||
|
.Select(entityChangeInfo => new EntityChange(GuidGenerator, auditLogId, entityChangeInfo, tenantId: auditLogInfo.TenantId)) |
||||
|
.ToList() |
||||
|
?? new List<EntityChange>(); |
||||
|
|
||||
|
var actions = auditLogInfo |
||||
|
.Actions? |
||||
|
.Select(auditLogActionInfo => new AuditLogAction(GuidGenerator.Create(), auditLogId, auditLogActionInfo, tenantId: auditLogInfo.TenantId)) |
||||
|
.ToList() |
||||
|
?? new List<AuditLogAction>(); |
||||
|
|
||||
|
var remoteServiceErrorInfos = auditLogInfo.Exceptions?.Select(exception => ExceptionToErrorInfoConverter.Convert(exception, true)) |
||||
|
?? new List<RemoteServiceErrorInfo>(); |
||||
|
|
||||
|
var exceptions = remoteServiceErrorInfos.Any() |
||||
|
? JsonSerializer.Serialize(remoteServiceErrorInfos, indented: true) |
||||
|
: null; |
||||
|
|
||||
|
var comments = auditLogInfo |
||||
|
.Comments? |
||||
|
.JoinAsString(Environment.NewLine); |
||||
|
|
||||
|
var auditLog = new AuditLog( |
||||
|
auditLogId, |
||||
|
auditLogInfo.ApplicationName, |
||||
|
auditLogInfo.TenantId, |
||||
|
auditLogInfo.TenantName, |
||||
|
auditLogInfo.UserId, |
||||
|
auditLogInfo.UserName, |
||||
|
auditLogInfo.ExecutionTime, |
||||
|
auditLogInfo.ExecutionDuration, |
||||
|
auditLogInfo.ClientIpAddress, |
||||
|
auditLogInfo.ClientName, |
||||
|
auditLogInfo.ClientId, |
||||
|
auditLogInfo.CorrelationId, |
||||
|
auditLogInfo.BrowserInfo, |
||||
|
auditLogInfo.HttpMethod, |
||||
|
auditLogInfo.Url, |
||||
|
auditLogInfo.HttpStatusCode, |
||||
|
auditLogInfo.ImpersonatorUserId, |
||||
|
auditLogInfo.ImpersonatorTenantId, |
||||
|
extraProperties, |
||||
|
entityChanges, |
||||
|
actions, |
||||
|
exceptions, |
||||
|
comments |
||||
|
); |
||||
|
|
||||
|
return Task.FromResult(auditLog); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Auditing; |
||||
|
|
||||
|
namespace Volo.Abp.AuditLogging |
||||
|
{ |
||||
|
public interface IAuditLogInfoToAuditLogConverter |
||||
|
{ |
||||
|
Task<AuditLog> ConvertAsync(AuditLogInfo auditLogInfo); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,33 @@ |
|||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
|
||||
|
namespace Volo.Abp.Identity.AspNetCore |
||||
|
{ |
||||
|
public class AbpSecurityStampValidatorCallback |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Implements callback for SecurityStampValidator's OnRefreshingPrincipal event.
|
||||
|
/// https://github.com/IdentityServer/IdentityServer4/blob/main/src/AspNetIdentity/src/SecurityStampValidatorCallback.cs
|
||||
|
/// </summary>
|
||||
|
public class SecurityStampValidatorCallback |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Maintains the claims captured at login time that are not being created by ASP.NET Identity.
|
||||
|
/// This is needed to preserve claims such as idp, auth_time, amr.
|
||||
|
/// </summary>
|
||||
|
/// <param name="context">The context.</param>
|
||||
|
/// <returns></returns>
|
||||
|
public static Task UpdatePrincipal(SecurityStampRefreshingPrincipalContext context) |
||||
|
{ |
||||
|
var newClaimTypes = context.NewPrincipal.Claims.Select(x => x.Type).ToArray(); |
||||
|
var currentClaimsToKeep = context.CurrentPrincipal.Claims.Where(x => !newClaimTypes.Contains(x.Type)).ToArray(); |
||||
|
|
||||
|
var id = context.NewPrincipal.Identities.First(); |
||||
|
id.AddClaims(currentClaimsToKeep); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,152 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Security.Claims; |
|
||||
using System.Threading.Tasks; |
|
||||
using IdentityServer4.Validation; |
|
||||
using Microsoft.Extensions.Localization; |
|
||||
using Microsoft.Extensions.Logging; |
|
||||
using Volo.Abp.Identity; |
|
||||
using Volo.Abp.IdentityServer.Localization; |
|
||||
using Volo.Abp.MultiTenancy; |
|
||||
using Volo.Abp.Security.Claims; |
|
||||
using Volo.Abp.Users; |
|
||||
using IdentityUser = Volo.Abp.Identity.IdentityUser; |
|
||||
|
|
||||
namespace Volo.Abp.IdentityServer.AspNetIdentity |
|
||||
{ |
|
||||
public class LinkLoginExtensionGrantValidator : IExtensionGrantValidator |
|
||||
{ |
|
||||
public const string ExtensionGrantType = "LinkLogin"; |
|
||||
|
|
||||
public string GrantType => ExtensionGrantType; |
|
||||
|
|
||||
protected ITokenValidator TokenValidator { get; } |
|
||||
protected IdentityLinkUserManager IdentityLinkUserManager { get; } |
|
||||
protected ICurrentTenant CurrentTenant { get; } |
|
||||
protected ICurrentUser CurrentUser { get; } |
|
||||
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor { get; } |
|
||||
protected IdentityUserManager UserManager { get; } |
|
||||
protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } |
|
||||
protected ILogger<LinkLoginExtensionGrantValidator> Logger { get; } |
|
||||
protected IStringLocalizer<AbpIdentityServerResource> Localizer { get; } |
|
||||
|
|
||||
public LinkLoginExtensionGrantValidator( |
|
||||
ITokenValidator tokenValidator, |
|
||||
IdentityLinkUserManager identityLinkUserManager, |
|
||||
ICurrentTenant currentTenant, |
|
||||
ICurrentUser currentUser, |
|
||||
IdentityUserManager userManager, |
|
||||
ICurrentPrincipalAccessor currentPrincipalAccessor, |
|
||||
IdentitySecurityLogManager identitySecurityLogManager, |
|
||||
ILogger<LinkLoginExtensionGrantValidator> logger, |
|
||||
IStringLocalizer<AbpIdentityServerResource> localizer) |
|
||||
{ |
|
||||
TokenValidator = tokenValidator; |
|
||||
IdentityLinkUserManager = identityLinkUserManager; |
|
||||
CurrentTenant = currentTenant; |
|
||||
CurrentUser = currentUser; |
|
||||
UserManager = userManager; |
|
||||
CurrentPrincipalAccessor = currentPrincipalAccessor; |
|
||||
IdentitySecurityLogManager = identitySecurityLogManager; |
|
||||
Logger = logger; |
|
||||
Localizer = localizer; |
|
||||
} |
|
||||
|
|
||||
public virtual async Task ValidateAsync(ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
var accessToken = context.Request.Raw["access_token"]; |
|
||||
if (accessToken.IsNullOrWhiteSpace()) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult |
|
||||
{ |
|
||||
IsError = true, |
|
||||
Error = "invalid_access_token" |
|
||||
}; |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var result = await TokenValidator.ValidateAccessTokenAsync(accessToken); |
|
||||
if (result.IsError) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult |
|
||||
{ |
|
||||
IsError = true, |
|
||||
Error = result.Error, |
|
||||
ErrorDescription = result.ErrorDescription |
|
||||
}; |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
using (CurrentPrincipalAccessor.Change(result.Claims)) |
|
||||
{ |
|
||||
if (!Guid.TryParse(context.Request.Raw["LinkUserId"], out var linkUserId)) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult |
|
||||
{ |
|
||||
IsError = true, |
|
||||
Error = "invalid_link_user_id" |
|
||||
}; |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
Guid? linkTenantId = null; |
|
||||
if (!context.Request.Raw["LinkTenantId"].IsNullOrWhiteSpace()) |
|
||||
{ |
|
||||
if (!Guid.TryParse(context.Request.Raw["LinkTenantId"], out var parsedGuid)) |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult |
|
||||
{ |
|
||||
IsError = true, |
|
||||
Error = "invalid_link_tenant_id" |
|
||||
}; |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
linkTenantId = parsedGuid; |
|
||||
} |
|
||||
|
|
||||
var isLinked = await IdentityLinkUserManager.IsLinkedAsync( |
|
||||
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
|
||||
new IdentityLinkUserInfo(linkUserId, linkTenantId), |
|
||||
true); |
|
||||
|
|
||||
if (isLinked) |
|
||||
{ |
|
||||
using (CurrentTenant.Change(linkTenantId)) |
|
||||
{ |
|
||||
var user = await UserManager.GetByIdAsync(linkUserId); |
|
||||
var sub = await UserManager.GetUserIdAsync(user); |
|
||||
|
|
||||
var additionalClaims = new List<Claim>(); |
|
||||
await AddCustomClaimsAsync(additionalClaims, user, context); |
|
||||
|
|
||||
context.Result = new GrantValidationResult( |
|
||||
sub, |
|
||||
GrantType, |
|
||||
additionalClaims.ToArray() |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
context.Result = new GrantValidationResult |
|
||||
{ |
|
||||
IsError = true, |
|
||||
Error = Localizer["TheTargetUserIsNotLinkedToYou"] |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected virtual Task AddCustomClaimsAsync(List<Claim> customClaims, IdentityUser user, ExtensionGrantValidationContext context) |
|
||||
{ |
|
||||
if (user.TenantId.HasValue) |
|
||||
{ |
|
||||
customClaims.Add(new Claim(AbpClaimTypes.TenantId, user.TenantId?.ToString())); |
|
||||
} |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,2 +1,4 @@ |
|||||
|
import * as Web from './web'; |
||||
export * from './account.service'; |
export * from './account.service'; |
||||
export * from './models'; |
export * from './models'; |
||||
|
export { Web }; |
||||
|
|||||
@ -0,0 +1,35 @@ |
|||||
|
import type { AbpLoginResult, UserLoginInfo } from './models/models'; |
||||
|
import { RestService } from '@abp/ng.core'; |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root', |
||||
|
}) |
||||
|
export class AccountService { |
||||
|
apiName = 'AbpAccount'; |
||||
|
|
||||
|
checkPasswordByLogin = (login: UserLoginInfo) => |
||||
|
this.restService.request<any, AbpLoginResult>({ |
||||
|
method: 'POST', |
||||
|
url: '/api/account/check-password', |
||||
|
body: login, |
||||
|
}, |
||||
|
{ apiName: this.apiName }); |
||||
|
|
||||
|
loginByLogin = (login: UserLoginInfo) => |
||||
|
this.restService.request<any, AbpLoginResult>({ |
||||
|
method: 'POST', |
||||
|
url: '/api/account/login', |
||||
|
body: login, |
||||
|
}, |
||||
|
{ apiName: this.apiName }); |
||||
|
|
||||
|
logout = () => |
||||
|
this.restService.request<any, void>({ |
||||
|
method: 'GET', |
||||
|
url: '/api/account/logout', |
||||
|
}, |
||||
|
{ apiName: this.apiName }); |
||||
|
|
||||
|
constructor(private restService: RestService) {} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
import * as Models from './models'; |
||||
|
export * from './account.service'; |
||||
|
export { Models }; |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './login-result-type.enum'; |
||||
|
export * from './models'; |
||||
@ -0,0 +1,11 @@ |
|||||
|
import { mapEnumToOptions } from '@abp/ng.core'; |
||||
|
|
||||
|
export enum LoginResultType { |
||||
|
Success = 1, |
||||
|
InvalidUserNameOrPassword = 2, |
||||
|
NotAllowed = 3, |
||||
|
LockedOut = 4, |
||||
|
RequiresTwoFactor = 5, |
||||
|
} |
||||
|
|
||||
|
export const loginResultTypeOptions = mapEnumToOptions(LoginResultType); |
||||
@ -0,0 +1,12 @@ |
|||||
|
import type { LoginResultType } from './login-result-type.enum'; |
||||
|
|
||||
|
export interface AbpLoginResult { |
||||
|
result: LoginResultType; |
||||
|
description?: string; |
||||
|
} |
||||
|
|
||||
|
export interface UserLoginInfo { |
||||
|
userNameOrEmailAddress: string; |
||||
|
password: string; |
||||
|
rememberMe: boolean; |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
import * as Controllers from './controllers'; |
||||
|
export { Controllers }; |
||||
@ -0,0 +1,2 @@ |
|||||
|
import * as Account from './account'; |
||||
|
export { Account }; |
||||
@ -0,0 +1,2 @@ |
|||||
|
import * as Areas from './areas'; |
||||
|
export { Areas }; |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,50 @@ |
|||||
|
{ |
||||
|
"root": true, |
||||
|
"ignorePatterns": [ |
||||
|
"projects/**/*" |
||||
|
], |
||||
|
"overrides": [ |
||||
|
{ |
||||
|
"files": [ |
||||
|
"*.ts" |
||||
|
], |
||||
|
"parserOptions": { |
||||
|
"project": [ |
||||
|
"tsconfig.json" |
||||
|
], |
||||
|
"createDefaultProgram": true |
||||
|
}, |
||||
|
"extends": [ |
||||
|
"plugin:@angular-eslint/recommended", |
||||
|
"plugin:@angular-eslint/template/process-inline-templates" |
||||
|
], |
||||
|
"rules": { |
||||
|
"@angular-eslint/directive-selector": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"type": "attribute", |
||||
|
"prefix": "app", |
||||
|
"style": "camelCase" |
||||
|
} |
||||
|
], |
||||
|
"@angular-eslint/component-selector": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"type": "element", |
||||
|
"prefix": "app", |
||||
|
"style": "kebab-case" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"files": [ |
||||
|
"*.html" |
||||
|
], |
||||
|
"extends": [ |
||||
|
"plugin:@angular-eslint/template/recommended" |
||||
|
], |
||||
|
"rules": {} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -1,32 +0,0 @@ |
|||||
// @ts-check
|
|
||||
// Protractor configuration file, see link for more information
|
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||
|
|
||||
const { SpecReporter } = require('jasmine-spec-reporter'); |
|
||||
|
|
||||
/** |
|
||||
* @type { import("protractor").Config } |
|
||||
*/ |
|
||||
exports.config = { |
|
||||
allScriptsTimeout: 11000, |
|
||||
specs: [ |
|
||||
'./src/**/*.e2e-spec.ts' |
|
||||
], |
|
||||
capabilities: { |
|
||||
browserName: 'chrome' |
|
||||
}, |
|
||||
directConnect: true, |
|
||||
baseUrl: 'http://localhost:4200/', |
|
||||
framework: 'jasmine', |
|
||||
jasmineNodeOpts: { |
|
||||
showColors: true, |
|
||||
defaultTimeoutInterval: 30000, |
|
||||
print: function() {} |
|
||||
}, |
|
||||
onPrepare() { |
|
||||
require('ts-node').register({ |
|
||||
project: require('path').join(__dirname, './tsconfig.json') |
|
||||
}); |
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); |
|
||||
} |
|
||||
}; |
|
||||
@ -1,23 +0,0 @@ |
|||||
import { AppPage } from './app.po'; |
|
||||
import { browser, logging } from 'protractor'; |
|
||||
|
|
||||
describe('workspace-project App', () => { |
|
||||
let page: AppPage; |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
page = new AppPage(); |
|
||||
}); |
|
||||
|
|
||||
it('should display welcome message', () => { |
|
||||
page.navigateTo(); |
|
||||
expect(page.getTitleText()).toEqual('ng9-abp app is running!'); |
|
||||
}); |
|
||||
|
|
||||
afterEach(async () => { |
|
||||
// Assert that there are no errors emitted from the browser
|
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER); |
|
||||
expect(logs).not.toContain(jasmine.objectContaining({ |
|
||||
level: logging.Level.SEVERE, |
|
||||
} as logging.Entry)); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,11 +0,0 @@ |
|||||
import { browser, by, element } from 'protractor'; |
|
||||
|
|
||||
export class AppPage { |
|
||||
navigateTo(): Promise<unknown> { |
|
||||
return browser.get(browser.baseUrl) as Promise<unknown>; |
|
||||
} |
|
||||
|
|
||||
getTitleText(): Promise<string> { |
|
||||
return element(by.css('app-root .content span')).getText() as Promise<string>; |
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
{ |
|
||||
"extends": "../tsconfig.json", |
|
||||
"compilerOptions": { |
|
||||
"outDir": "../out-tsc/e2e", |
|
||||
"module": "commonjs", |
|
||||
"target": "es2018", |
|
||||
"types": [ |
|
||||
"jasmine", |
|
||||
"jasminewd2", |
|
||||
"node" |
|
||||
] |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +1,15 @@ |
|||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
||||
{ |
{ |
||||
"extends": "./tsconfig.json", |
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
"compilerOptions": { |
||||
"outDir": "./out-tsc/app", |
"outDir": "./out-tsc/app", |
||||
"types": [] |
"types": [] |
||||
}, |
}, |
||||
"files": ["src/main.ts", "src/polyfills.ts"], |
"files": [ |
||||
"include": ["src/**/*.d.ts"] |
"src/main.ts", |
||||
|
"src/polyfills.ts" |
||||
|
], |
||||
|
"include": [ |
||||
|
"src/**/*.d.ts" |
||||
|
] |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue