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 './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", |
|||
"compilerOptions": { |
|||
"outDir": "./out-tsc/app", |
|||
"types": [] |
|||
}, |
|||
"files": ["src/main.ts", "src/polyfills.ts"], |
|||
"include": ["src/**/*.d.ts"] |
|||
"files": [ |
|||
"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