Added skoruba api integration

This commit is contained in:
2026-10-04 14:31:06 +02:00
parent b3957f4af4
commit 06539236a1
151 changed files with 317 additions and 242 deletions
@@ -1,7 +1,7 @@
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Errors;
namespace PostFundManagement.Application.Abstractions;
namespace PostFundManagement.Application.Abstractions.Services;
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
{
@@ -1,4 +1,4 @@
namespace PostFundManagement.Application.Abstractions;
namespace PostFundManagement.Application.Abstractions.Services;
public abstract class ServiceCacheBase
{
@@ -1,6 +1,7 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Security;
using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Attributes;
using PostFundManagement.Domain.Configuration.Security;
using PostFundManagement.Infrastructure.Security;
namespace PostFundManagement.Application.Extensions;
@@ -1,13 +1,53 @@
using PostFundManagement.Application.Interceptors;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Configuration.Security;
using PostFundManagement.Domain.Extensions;
using PostFundManagement.Domain.Security.Sdk;
using PostFundManagement.Infrastructure.Database;
using PostFundManagement.Infrastructure.Security;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Extensions;
public static class Security
{
public static IServiceCollection AddSkorubaApiService(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(MachineIdentitySettings));
var authOptions = new MachineIdentitySettings();
configSection.Bind(authOptions);
services.Configure<MachineIdentitySettings>(configSection);
if (string.IsNullOrWhiteSpace(authOptions.Authority))
return services;
if (!authOptions.Authority.EndsWith('/')) authOptions.Authority += "/";
services.AddClientCredentialsTokenManagement()
.AddClient(SkorubaCredentialClient, client =>
{
client.TokenEndpoint = new Uri($"{authOptions.Authority}connect/token");
client.ClientId = ClientId.Parse(authOptions.ClientId!);
client.ClientSecret = ClientSecret.Parse(authOptions.ClientSecret!);
client.Scope = Duende.AccessTokenManagement.Scope.ParseOrDefault(authOptions.Scope);
});
services.AddHttpClient(SkorubaHttpClient, client =>
{
client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!);
}).AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse(SkorubaCredentialClient));
services.AddScoped<AuditInterceptor>();
services.AddScoped<LoggingInterceptor>();
services.AddSingleton<ProxyGenerator>();
services.AddLoggedAuditedServiceScoped<ISkorubaUserService, SkorubaUserService>();
return services;
}
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
@@ -0,0 +1,78 @@
using PostFundManagement.Application.Interceptors;
namespace PostFundManagement.Application.Extensions;
public static class ServiceExtensionTools
{
public static IServiceCollection AddLoggedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(provider =>
{
var proxyGenerator = provider.GetRequiredService<ProxyGenerator>();
var loggingInterceptor = provider.GetRequiredService<LoggingInterceptor>();
var actual = provider.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(provider, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(provider =>
{
var proxyGenerator = provider.GetRequiredService<ProxyGenerator>();
var auditInterceptor = provider.GetRequiredService<AuditInterceptor>();
var loggingInterceptor = provider.GetRequiredService<LoggingInterceptor>();
var actual = provider.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(provider, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor(),
auditInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddLoggedAuditedServiceScoped<TInterface, TImplementation>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped(provider =>
{
var proxyGenerator = provider.GetRequiredService<ProxyGenerator>();
var auditInterceptor = provider.GetRequiredService<AuditInterceptor>();
var loggingInterceptor = provider.GetRequiredService<LoggingInterceptor>();
var actual = provider.GetRequiredService<TImplementation>();
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(actual,
loggingInterceptor.ToInterceptor(),
auditInterceptor.ToInterceptor());
});
return services;
}
}
@@ -15,56 +15,6 @@ namespace PostFundManagement.Application.Extensions;
public static class Services
{
public static IServiceCollection AddLoggedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddLoggedAuditedCachedServiceScoped<TInterface, TImplementation, TCacheDecorator>(this IServiceCollection services)
where TInterface : class
where TImplementation : class, TInterface
where TCacheDecorator : class, TInterface
{
services.AddScoped<TImplementation>();
services.AddScoped<TCacheDecorator>();
services.AddScoped(sp =>
{
var proxyGenerator = sp.GetRequiredService<ProxyGenerator>();
var auditInterceptor = sp.GetRequiredService<AuditInterceptor>();
var loggingInterceptor = sp.GetRequiredService<LoggingInterceptor>();
var actual = sp.GetRequiredService<TImplementation>();
var cached = ActivatorUtilities.CreateInstance<TCacheDecorator>(sp, actual);
return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(cached,
loggingInterceptor.ToInterceptor(),
auditInterceptor.ToInterceptor());
});
return services;
}
public static IServiceCollection AddRegisterServicesWithCaching(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<AuditInterceptor>();
@@ -25,6 +25,8 @@
<!-- API Documentation -->
<ItemGroup>
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageReference Include="Duende.AccessTokenManagement" Version="4.2.0" />
<PackageReference Include="Duende.AccessTokenManagement.OpenIdConnect" Version="4.2.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Validation" Version="10.0.12" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.4" />
@@ -37,6 +39,8 @@
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Humanizer" />
<Using Include="Duende.AccessTokenManagement.OpenIdConnect" />
<Using Include="Duende.AccessTokenManagement" />
<Using Include="Castle.DynamicProxy" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.ComponentModel.DataAnnotations" />
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;
@@ -1,4 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Domain.Abstractions;
using static PostFundManagement.Domain.Extensions.Constants;
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
namespace PostFundManagement.Application.Services.Shared;
@@ -12,9 +12,10 @@ public sealed class IdentityService(IDbContextFactory<ApplicationDbContext> cont
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
// TODO: Ensure TenantUser status is checked
var userExistsAndActive = await context.Users
.AnyAsync(u => u.Id == userId && u.Status == ActivityStatus.Active, cancellationToken);
var userExistsAndActive = await context.TenantUsers
.Include(i => i.User)
.AnyAsync(u => u.UserId == userId && u.Status == ActivityStatus.Active &&
u.User!.Status == ActivityStatus.Active, cancellationToken);
return Result.OkIf(userExistsAndActive, NotFoundError.Create());
}));
@@ -1,7 +1,7 @@
using PostFundManagement.Domain.Configuration.Security;
using PostFundManagement.Domain.Errors;
using PostFundManagement.Domain.Security.Configuration;
using PostFundManagement.Domain.Security.Models;
using PostFundManagement.Domain.Security.Sdk;
using PostFundManagement.Domain.Models.Security;
using PostFundManagement.Infrastructure.Security;
namespace PostFundManagement.Application.Services.Shared;
@@ -9,10 +9,10 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
{
private readonly SecurityClientSettings clientSettings = clientOptions.Value;
public async Task<Result<Domain.Security.Models.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
public async Task<Result<Domain.Models.Security.TokenResponse>> GenerateAsync(CancellationToken cancellationToken = default) =>
await Result.Try((Func<Task<Result<Domain.Models.Security.TokenResponse>>>)(async () =>
{
using var response = await connectApi.GetToken(new Domain.Security.Models.TokenRequest
using var response = await connectApi.GetToken(new Domain.Models.Security.TokenRequest
{
ClientId = clientSettings.ClientId,
ClientSecret = clientSettings.ClientSecret,
@@ -27,14 +27,14 @@ public sealed class TokenService(ISecurityConnectApi connectApi, IOptions<Securi
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonSerializer.Deserialize<Domain.Security.Models.TokenResponse>(contentRaw);
var tokenResponse = JsonSerializer.Deserialize<Domain.Models.Security.TokenResponse>(contentRaw);
return Result.FailIf(string.IsNullOrWhiteSpace(tokenResponse?.AccessToken),
NotFoundError.Create("Authentication succeeded, but no access token was found in the response payload."))
.ToResult(tokenResponse!);
}
return await Result.Try((Func<Task<Result<Domain.Security.Models.TokenResponse>>>)(async () =>
return await Result.Try((Func<Task<Result<Domain.Models.Security.TokenResponse>>>)(async () =>
{
var errorResult = JsonSerializer.Deserialize<TokenErrorResponse>(contentRaw);
@@ -1,5 +1,4 @@
using PostFundManagement.Application.Abstractions;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Abstractions.Services;
using PostFundManagement.Application.Extensions;
using PostFundManagement.Application.Services.Shared;
using PostFundManagement.Domain;