Moved DI extensions to Appl;ication layer
This commit is contained in:
@@ -16,4 +16,33 @@ public static class General
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static Guid GetUserId(this HttpContext context) => (Guid)context.Items[Domain.Extensions.Constants.UserIdItemKey]!;
|
||||
|
||||
public static string GetUserEmail(this HttpContext context) => (string)context.Items[Domain.Extensions.Constants.EmaiItemKey]!;
|
||||
|
||||
public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal)
|
||||
{
|
||||
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
|
||||
|
||||
var decoded = Guid.TryParse(sidClaim, out var userId);
|
||||
|
||||
return (decoded, userId);
|
||||
}
|
||||
|
||||
public static (bool Decoded, Guid email) GetUserEmail(this ClaimsPrincipal principal)
|
||||
{
|
||||
var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value;
|
||||
|
||||
var decoded = Guid.TryParse(emailClaim, out var email);
|
||||
|
||||
return (decoded, email);
|
||||
}
|
||||
|
||||
public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList<IError> errors) => new()
|
||||
{
|
||||
Detail = string.Join(',', errors.Select(e => e.Message)),
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Title = errors[0]?.Message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using OpenTelemetry.Exporter;
|
||||
using OpenTelemetry.Logs;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using OpenTelemetry.Metrics;
|
||||
|
||||
namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Otel
|
||||
{
|
||||
public static WebApplicationBuilder AddWebTelemetry(this WebApplicationBuilder builder)
|
||||
{
|
||||
var serviceName = builder.Configuration.GetValue<string>("Monitoring:ServiceName") ?? "Pfm";
|
||||
var endpoint = builder.Configuration.GetValue<string>("Monitoring:Address")!;
|
||||
var apiKey = builder.Configuration.GetValue<string>("Monitoring:ApiKey");
|
||||
|
||||
var resourceBuilder = ResourceBuilder.CreateDefault()
|
||||
.AddService(serviceName);
|
||||
|
||||
var otlpHeaders = !string.IsNullOrEmpty(apiKey) ? $"x-otlp-api-key={apiKey}" : null;
|
||||
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.SetResourceBuilder(resourceBuilder);
|
||||
logging.AddOtlpExporter(opt =>
|
||||
{
|
||||
opt.Endpoint = new Uri(endpoint);
|
||||
opt.Protocol = OtlpExportProtocol.Grpc;
|
||||
opt.Headers = otlpHeaders;
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithTracing(tracing => tracing
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddOtlpExporter(opt =>
|
||||
{
|
||||
opt.Endpoint = new Uri(endpoint);
|
||||
opt.Protocol = OtlpExportProtocol.Grpc;
|
||||
opt.Headers = otlpHeaders;
|
||||
}))
|
||||
.WithMetrics(metrics => metrics
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddMeter(serviceName)
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddRuntimeInstrumentation()
|
||||
.AddOtlpExporter(opt =>
|
||||
{
|
||||
opt.Endpoint = new Uri(endpoint);
|
||||
opt.Protocol = OtlpExportProtocol.Grpc;
|
||||
opt.Headers = otlpHeaders;
|
||||
}));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using PostFundManagement.Infrastructure.Database;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Postgres
|
||||
{
|
||||
public static IServiceCollection AddDataProtectionDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
|
||||
|
||||
services.AddPooledDbContextFactory<DataProtectionDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(DatabaseConfigName)
|
||||
?? throw new InvalidOperationException($"Connection string '{DatabaseConfigName}' was not found in configuration.");
|
||||
|
||||
services.AddPooledDbContextFactory<ApplicationDbContext>(builder => builder.UseNpgsql(connectionString));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
using PostFundManagement.Domain.Quartz;
|
||||
using static PostFundManagement.Domain.Extensions.Constants;
|
||||
|
||||
namespace PostFundManagement.Domain.Extensions;
|
||||
|
||||
public static class Quartz
|
||||
{
|
||||
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
|
||||
|
||||
services.ConfigureCommon();
|
||||
|
||||
services.AddQuartz(config =>
|
||||
{
|
||||
config.SchedulerName = schedulerName;
|
||||
config.SchedulerId = "AUTO";
|
||||
|
||||
config.UseSimpleTypeLoader();
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
|
||||
config.UseTimeZoneConverter();
|
||||
|
||||
config.UsePersistentStore(storage =>
|
||||
{
|
||||
storage.PerformSchemaValidation = false;
|
||||
|
||||
storage.UseSystemTextJsonSerializer();
|
||||
storage.SetProperty("quartz.jobStore.clustered", "true");
|
||||
|
||||
storage.UsePostgres(options =>
|
||||
{
|
||||
options.ConnectionString = connectionString!;
|
||||
options.TablePrefix = "quartz.qrtz_";
|
||||
});
|
||||
storage.UseClustering(cluster =>
|
||||
{
|
||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
|
||||
|
||||
services.ConfigureCommon();
|
||||
|
||||
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
|
||||
|
||||
services.AddQuartz(config =>
|
||||
{
|
||||
config.SchedulerName = schedulerName;
|
||||
config.SchedulerId = "AUTO";
|
||||
config.InterruptJobsOnShutdown = true;
|
||||
config.InterruptJobsOnShutdownWithWait = true;
|
||||
config.MaxBatchSize = 5;
|
||||
|
||||
config.UseSimpleTypeLoader();
|
||||
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
|
||||
config.UseTimeZoneConverter();
|
||||
|
||||
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
config.UsePersistentStore(storage =>
|
||||
{
|
||||
storage.PerformSchemaValidation = false;
|
||||
|
||||
storage.UseSystemTextJsonSerializer();
|
||||
storage.SetProperty("quartz.jobStore.clustered", "true");
|
||||
|
||||
storage.UsePostgres(options =>
|
||||
{
|
||||
options.ConnectionString = connectionString!;
|
||||
options.TablePrefix = "quartz.qrtz_";
|
||||
});
|
||||
storage.UseClustering(cluster =>
|
||||
{
|
||||
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
|
||||
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection ConfigureCommon(this IServiceCollection services)
|
||||
{
|
||||
services.Configure<QuartzOptions>(options =>
|
||||
{
|
||||
options.Scheduling.IgnoreDuplicates = true;
|
||||
options.Scheduling.OverWriteExistingData = true;
|
||||
|
||||
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
|
||||
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
|
||||
});
|
||||
|
||||
services.AddTransient<RetryJobListener>();
|
||||
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using PostFundManagement.Application.Shared;
|
||||
using PostFundManagement.Domain.Api.Configuration;
|
||||
using PostFundManagement.Domain.Sdk;
|
||||
using PostFundManagement.Infrastructure.Database;
|
||||
|
||||
namespace PostFundManagement.Application.Extensions;
|
||||
|
||||
public static class Security
|
||||
{
|
||||
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
|
||||
|
||||
var authOptions = new SecurityClientSettings();
|
||||
configSection.Bind(authOptions);
|
||||
|
||||
services.Configure<SecurityClientSettings>(configSection);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authOptions.Authority))
|
||||
return services;
|
||||
|
||||
if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/";
|
||||
|
||||
services.AddRefitClient<ISecurityConnectApi>()
|
||||
.ConfigureHttpClient(config =>
|
||||
{
|
||||
config.BaseAddress = new Uri(authOptions.Authority);
|
||||
config.Timeout = TimeSpan.FromSeconds(15);
|
||||
})
|
||||
.AddStandardResilienceHandler(options =>
|
||||
{
|
||||
options.Retry.MaxRetryAttempts = 3;
|
||||
options.Retry.Delay = TimeSpan.FromSeconds(1);
|
||||
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
|
||||
});
|
||||
|
||||
services.AddScoped<TokenService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddWebSecurity(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddAuthorization();
|
||||
|
||||
var certString = configuration["DataProtection:Certificate"] ?? configuration["DataProtection__Certificate"];
|
||||
var certPassword = configuration["DataProtection:Password"] ?? configuration["DataProtection__Password"];
|
||||
|
||||
if (string.IsNullOrEmpty(certString))
|
||||
throw new InvalidOperationException("Data Protection Certificate configuration is missing.");
|
||||
|
||||
var certificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(certString), certPassword);
|
||||
|
||||
services.AddDataProtection().PersistKeysToDbContext<DataProtectionDbContext>()
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.SetApplicationName("PfmApp");
|
||||
|
||||
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "PfmApp");
|
||||
|
||||
services.ConfigureCookieOidcSameSiteSupport();
|
||||
|
||||
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
|
||||
|
||||
var authOptions = new SecurityClientSettings();
|
||||
configSection.Bind(authOptions);
|
||||
|
||||
services.Configure<SecurityClientSettings>(configSection);
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.Cookie.Name = "PfmApp.Session";
|
||||
})
|
||||
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Authority = authOptions.Authority;
|
||||
options.ClientId = authOptions.ClientId;
|
||||
options.ClientSecret = authOptions.ClientSecret;
|
||||
options.ResponseType = "code";
|
||||
|
||||
options.SaveTokens = true;
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
options.CorrelationCookie.SameSite = SameSiteMode.None;
|
||||
|
||||
options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
options.NonceCookie.SameSite = SameSiteMode.None;
|
||||
|
||||
options.ForwardSignOut = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
|
||||
options.Scope.Clear();
|
||||
options.Scope.Add("openid");
|
||||
options.Scope.Add("profile");
|
||||
options.Scope.Add("email");
|
||||
|
||||
options.Events = new OpenIdConnectEvents
|
||||
{
|
||||
OnRedirectToIdentityProviderForSignOut = context =>
|
||||
{
|
||||
var idToken = context.ProtocolMessage.IdTokenHint;
|
||||
|
||||
if (string.IsNullOrEmpty(idToken))
|
||||
{
|
||||
var tokens = context.Properties.GetTokens();
|
||||
var idTokenItem = tokens.FirstOrDefault(t => string.Equals(t.Name, "id_token", StringComparison.Ordinal));
|
||||
|
||||
if (idTokenItem != null) context.ProtocolMessage.IdTokenHint = idTokenItem.Value;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
services.AddCascadingAuthenticationState();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApiSecurity(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var configSection = configuration.GetSection(nameof(SecuritySettings));
|
||||
|
||||
var authOptions = new SecuritySettings();
|
||||
configSection.Bind(authOptions);
|
||||
|
||||
services.Configure<SecuritySettings>(configSection);
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = authOptions.Authority;
|
||||
options.Audience = authOptions.Audience;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = authOptions.Authority,
|
||||
ValidateAudience = true,
|
||||
ValidateIssuer = true,
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user